diff --git a/.gitignore b/.gitignore
index 3369bad9..04dd567c 100644
--- a/.gitignore
+++ b/.gitignore
@@ -4,8 +4,15 @@ env/
__pycache__/
.DS_Store
*.csv
-src/
+/src/
eval_results/
eval_data/
*.egg-info/
.env
+
+# Node.js
+node_modules/
+package-lock.json
+
+# Frontend dev artifacts
+.frontend-dev/
diff --git a/cli/main.py b/cli/main.py
index 2e06d50c..3f4ddc0c 100644
--- a/cli/main.py
+++ b/cli/main.py
@@ -26,6 +26,7 @@ from rich.rule import Rule
from tradingagents.graph.trading_graph import TradingAgentsGraph
from tradingagents.default_config import DEFAULT_CONFIG
+from tradingagents.dataflows.markets import is_nifty_50_stock, NIFTY_50_STOCKS
from cli.models import AnalystType
from cli.utils import *
@@ -429,29 +430,42 @@ def get_user_selections():
box_content += f"\n[dim]Default: {default}[/dim]"
return Panel(box_content, border_style="blue", padding=(1, 2))
- # Step 1: Ticker symbol
+ # Step 1: Market selection
console.print(
create_question_box(
- "Step 1: Ticker Symbol", "Enter the ticker symbol to analyze", "SPY"
+ "Step 1: Market Selection", "Select the market for your analysis"
)
)
- selected_ticker = get_ticker()
+ selected_market = select_market()
- # Step 2: Analysis date
+ # Show Nifty 50 stocks if Indian market is selected
+ if selected_market == "india_nse":
+ show_nifty_50_stocks()
+
+ # Step 2: Ticker symbol
+ console.print(
+ create_question_box(
+ "Step 2: Ticker Symbol", "Enter the ticker symbol to analyze",
+ "RELIANCE" if selected_market == "india_nse" else "SPY"
+ )
+ )
+ selected_ticker = get_ticker_with_market_hint(selected_market)
+
+ # Step 3: Analysis date
default_date = datetime.datetime.now().strftime("%Y-%m-%d")
console.print(
create_question_box(
- "Step 2: Analysis Date",
+ "Step 3: Analysis Date",
"Enter the analysis date (YYYY-MM-DD)",
default_date,
)
)
analysis_date = get_analysis_date()
- # Step 3: Select analysts
+ # Step 4: Select analysts
console.print(
create_question_box(
- "Step 3: Analysts Team", "Select your LLM analyst agents for the analysis"
+ "Step 4: Analysts Team", "Select your LLM analyst agents for the analysis"
)
)
selected_analysts = select_analysts()
@@ -459,26 +473,26 @@ def get_user_selections():
f"[green]Selected analysts:[/green] {', '.join(analyst.value for analyst in selected_analysts)}"
)
- # Step 4: Research depth
+ # Step 5: Research depth
console.print(
create_question_box(
- "Step 4: Research Depth", "Select your research depth level"
+ "Step 5: Research Depth", "Select your research depth level"
)
)
selected_research_depth = select_research_depth()
- # Step 5: OpenAI backend
+ # Step 6: OpenAI backend
console.print(
create_question_box(
- "Step 5: OpenAI backend", "Select which service to talk to"
+ "Step 6: LLM Provider", "Select which service to talk to"
)
)
selected_llm_provider, backend_url = select_llm_provider()
-
- # Step 6: Thinking agents
+
+ # Step 7: Thinking agents
console.print(
create_question_box(
- "Step 6: Thinking Agents", "Select your thinking agents for analysis"
+ "Step 7: Thinking Agents", "Select your thinking agents for analysis"
)
)
selected_shallow_thinker = select_shallow_thinking_agent(selected_llm_provider)
@@ -493,6 +507,7 @@ def get_user_selections():
"backend_url": backend_url,
"shallow_thinker": selected_shallow_thinker,
"deep_thinker": selected_deep_thinker,
+ "market": selected_market,
}
@@ -747,6 +762,13 @@ def run_analysis():
config["deep_think_llm"] = selections["deep_thinker"]
config["backend_url"] = selections["backend_url"]
config["llm_provider"] = selections["llm_provider"].lower()
+ config["market"] = selections["market"]
+
+ # Display market info for NSE stocks
+ if is_nifty_50_stock(selections["ticker"]):
+ company_name = NIFTY_50_STOCKS.get(selections["ticker"].replace(".NS", ""), "")
+ console.print(f"[cyan]Analyzing NSE stock:[/cyan] {selections['ticker']} - {company_name}")
+ console.print("[dim]Using jugaad-data for NSE stock data, yfinance for fundamentals[/dim]")
# Initialize the graph
graph = TradingAgentsGraph(
@@ -808,10 +830,17 @@ def run_analysis():
update_display(layout)
# Add initial messages
- message_buffer.add_message("System", f"Selected ticker: {selections['ticker']}")
+ ticker_info = selections['ticker']
+ if is_nifty_50_stock(selections['ticker']):
+ company_name = NIFTY_50_STOCKS.get(selections['ticker'].replace(".NS", ""), "")
+ ticker_info = f"{selections['ticker']} ({company_name}) [NSE]"
+ message_buffer.add_message("System", f"Selected ticker: {ticker_info}")
message_buffer.add_message(
"System", f"Analysis date: {selections['analysis_date']}"
)
+ message_buffer.add_message(
+ "System", f"Market: {selections['market'].upper()}"
+ )
message_buffer.add_message(
"System",
f"Selected analysts: {', '.join(analyst.value for analyst in selections['analysts'])}",
diff --git a/cli/utils.py b/cli/utils.py
index 7b9682a6..93f6d7aa 100644
--- a/cli/utils.py
+++ b/cli/utils.py
@@ -1,7 +1,13 @@
import questionary
from typing import List, Optional, Tuple, Dict
+from rich.console import Console
+from rich.table import Table
+from rich import box
from cli.models import AnalystType
+from tradingagents.dataflows.markets import NIFTY_50_STOCKS, is_nifty_50_stock
+
+console = Console()
ANALYST_ORDER = [
("Market Analyst", AnalystType.MARKET),
@@ -272,5 +278,117 @@ def select_llm_provider() -> tuple[str, str]:
display_name, url = choice
print(f"You selected: {display_name}\tURL: {url}")
-
+
return display_name, url
+
+
+def select_market() -> str:
+ """Select market using an interactive selection."""
+
+ MARKET_OPTIONS = [
+ ("Auto-detect (Recommended)", "auto"),
+ ("US Markets (NYSE, NASDAQ)", "us"),
+ ("Indian NSE (Nifty 50)", "india_nse"),
+ ]
+
+ choice = questionary.select(
+ "Select Your [Market]:",
+ choices=[
+ questionary.Choice(display, value=value)
+ for display, value in MARKET_OPTIONS
+ ],
+ instruction="\n- Use arrow keys to navigate\n- Press Enter to select",
+ style=questionary.Style(
+ [
+ ("selected", "fg:cyan noinherit"),
+ ("highlighted", "fg:cyan noinherit"),
+ ("pointer", "fg:cyan noinherit"),
+ ]
+ ),
+ ).ask()
+
+ if choice is None:
+ console.print("\n[red]No market selected. Exiting...[/red]")
+ exit(1)
+
+ return choice
+
+
+def display_nifty_50_stocks():
+ """Display the list of Nifty 50 stocks in a formatted table."""
+ table = Table(
+ title="Nifty 50 Stocks",
+ box=box.ROUNDED,
+ show_header=True,
+ header_style="bold cyan",
+ )
+
+ table.add_column("Symbol", style="green", width=15)
+ table.add_column("Company Name", style="white", width=45)
+
+ # Sort stocks alphabetically
+ sorted_stocks = sorted(NIFTY_50_STOCKS.items())
+
+ for symbol, company_name in sorted_stocks:
+ table.add_row(symbol, company_name)
+
+ console.print(table)
+ console.print()
+
+
+def show_nifty_50_stocks() -> bool:
+ """Ask user if they want to see Nifty 50 stocks list."""
+ show = questionary.confirm(
+ "Would you like to see the list of Nifty 50 stocks?",
+ default=False,
+ style=questionary.Style(
+ [
+ ("selected", "fg:cyan noinherit"),
+ ("highlighted", "fg:cyan noinherit"),
+ ]
+ ),
+ ).ask()
+
+ if show:
+ display_nifty_50_stocks()
+
+ return show
+
+
+def get_ticker_with_market_hint(market: str) -> str:
+ """Get ticker symbol with market-specific hints."""
+ if market == "india_nse":
+ hint = "Enter NSE symbol (e.g., RELIANCE, TCS, INFY)"
+ default = "RELIANCE"
+ elif market == "us":
+ hint = "Enter US ticker symbol (e.g., AAPL, GOOGL, MSFT)"
+ default = "SPY"
+ else:
+ hint = "Enter ticker symbol (auto-detects market)"
+ default = "SPY"
+
+ ticker = questionary.text(
+ hint + ":",
+ default=default,
+ validate=lambda x: len(x.strip()) > 0 or "Please enter a valid ticker symbol.",
+ style=questionary.Style(
+ [
+ ("text", "fg:green"),
+ ("highlighted", "noinherit"),
+ ]
+ ),
+ ).ask()
+
+ if not ticker:
+ console.print("\n[red]No ticker symbol provided. Exiting...[/red]")
+ exit(1)
+
+ ticker = ticker.strip().upper()
+
+ # Provide feedback for NSE stocks
+ if is_nifty_50_stock(ticker):
+ company_name = NIFTY_50_STOCKS.get(ticker.replace(".NS", ""), "")
+ if company_name:
+ console.print(f"[green]Detected NSE stock:[/green] {ticker} - {company_name}")
+
+ return ticker
diff --git a/frontend/.gitignore b/frontend/.gitignore
new file mode 100644
index 00000000..a547bf36
--- /dev/null
+++ b/frontend/.gitignore
@@ -0,0 +1,24 @@
+# Logs
+logs
+*.log
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+pnpm-debug.log*
+lerna-debug.log*
+
+node_modules
+dist
+dist-ssr
+*.local
+
+# Editor directories and files
+.vscode/*
+!.vscode/extensions.json
+.idea
+.DS_Store
+*.suo
+*.ntvs*
+*.njsproj
+*.sln
+*.sw?
diff --git a/frontend/README.md b/frontend/README.md
new file mode 100644
index 00000000..d2e77611
--- /dev/null
+++ b/frontend/README.md
@@ -0,0 +1,73 @@
+# React + TypeScript + Vite
+
+This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules.
+
+Currently, two official plugins are available:
+
+- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react) uses [Babel](https://babeljs.io/) (or [oxc](https://oxc.rs) when used in [rolldown-vite](https://vite.dev/guide/rolldown)) for Fast Refresh
+- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh
+
+## React Compiler
+
+The React Compiler is not enabled on this template because of its impact on dev & build performances. To add it, see [this documentation](https://react.dev/learn/react-compiler/installation).
+
+## Expanding the ESLint configuration
+
+If you are developing a production application, we recommend updating the configuration to enable type-aware lint rules:
+
+```js
+export default defineConfig([
+ globalIgnores(['dist']),
+ {
+ files: ['**/*.{ts,tsx}'],
+ extends: [
+ // Other configs...
+
+ // Remove tseslint.configs.recommended and replace with this
+ tseslint.configs.recommendedTypeChecked,
+ // Alternatively, use this for stricter rules
+ tseslint.configs.strictTypeChecked,
+ // Optionally, add this for stylistic rules
+ tseslint.configs.stylisticTypeChecked,
+
+ // Other configs...
+ ],
+ languageOptions: {
+ parserOptions: {
+ project: ['./tsconfig.node.json', './tsconfig.app.json'],
+ tsconfigRootDir: import.meta.dirname,
+ },
+ // other options...
+ },
+ },
+])
+```
+
+You can also install [eslint-plugin-react-x](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-x) and [eslint-plugin-react-dom](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-dom) for React-specific lint rules:
+
+```js
+// eslint.config.js
+import reactX from 'eslint-plugin-react-x'
+import reactDom from 'eslint-plugin-react-dom'
+
+export default defineConfig([
+ globalIgnores(['dist']),
+ {
+ files: ['**/*.{ts,tsx}'],
+ extends: [
+ // Other configs...
+ // Enable lint rules for React
+ reactX.configs['recommended-typescript'],
+ // Enable lint rules for React DOM
+ reactDom.configs.recommended,
+ ],
+ languageOptions: {
+ parserOptions: {
+ project: ['./tsconfig.node.json', './tsconfig.app.json'],
+ tsconfigRootDir: import.meta.dirname,
+ },
+ // other options...
+ },
+ },
+])
+```
diff --git a/frontend/backend/database.py b/frontend/backend/database.py
new file mode 100644
index 00000000..1353800c
--- /dev/null
+++ b/frontend/backend/database.py
@@ -0,0 +1,223 @@
+"""SQLite database module for storing stock recommendations."""
+import sqlite3
+import json
+from pathlib import Path
+from datetime import datetime
+from typing import Optional
+
+DB_PATH = Path(__file__).parent / "recommendations.db"
+
+
+def get_connection():
+ """Get SQLite database connection."""
+ conn = sqlite3.connect(DB_PATH)
+ conn.row_factory = sqlite3.Row
+ return conn
+
+
+def init_db():
+ """Initialize the database with required tables."""
+ conn = get_connection()
+ cursor = conn.cursor()
+
+ # Create recommendations table
+ cursor.execute("""
+ CREATE TABLE IF NOT EXISTS daily_recommendations (
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
+ date TEXT UNIQUE NOT NULL,
+ summary_total INTEGER,
+ summary_buy INTEGER,
+ summary_sell INTEGER,
+ summary_hold INTEGER,
+ top_picks TEXT,
+ stocks_to_avoid TEXT,
+ created_at TEXT DEFAULT CURRENT_TIMESTAMP
+ )
+ """)
+
+ # Create stock analysis table
+ cursor.execute("""
+ CREATE TABLE IF NOT EXISTS stock_analysis (
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
+ date TEXT NOT NULL,
+ symbol TEXT NOT NULL,
+ company_name TEXT,
+ decision TEXT,
+ confidence TEXT,
+ risk TEXT,
+ raw_analysis TEXT,
+ created_at TEXT DEFAULT CURRENT_TIMESTAMP,
+ UNIQUE(date, symbol)
+ )
+ """)
+
+ # Create index for faster queries
+ cursor.execute("""
+ CREATE INDEX IF NOT EXISTS idx_stock_analysis_date ON stock_analysis(date)
+ """)
+ cursor.execute("""
+ CREATE INDEX IF NOT EXISTS idx_stock_analysis_symbol ON stock_analysis(symbol)
+ """)
+
+ conn.commit()
+ 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."""
+ conn = get_connection()
+ cursor = conn.cursor()
+
+ try:
+ # Insert or replace daily recommendation
+ cursor.execute("""
+ INSERT OR REPLACE INTO daily_recommendations
+ (date, summary_total, summary_buy, summary_sell, summary_hold, top_picks, stocks_to_avoid)
+ VALUES (?, ?, ?, ?, ?, ?, ?)
+ """, (
+ date,
+ summary.get('total', 0),
+ summary.get('buy', 0),
+ summary.get('sell', 0),
+ summary.get('hold', 0),
+ json.dumps(top_picks),
+ json.dumps(stocks_to_avoid)
+ ))
+
+ # Insert stock analysis for each stock
+ for symbol, analysis in analysis_data.items():
+ cursor.execute("""
+ INSERT OR REPLACE INTO stock_analysis
+ (date, symbol, company_name, decision, confidence, risk, raw_analysis)
+ VALUES (?, ?, ?, ?, ?, ?, ?)
+ """, (
+ date,
+ symbol,
+ analysis.get('company_name', ''),
+ analysis.get('decision'),
+ analysis.get('confidence'),
+ analysis.get('risk'),
+ analysis.get('raw_analysis', '')
+ ))
+
+ conn.commit()
+ finally:
+ conn.close()
+
+
+def get_recommendation_by_date(date: str) -> Optional[dict]:
+ """Get recommendation for a specific date."""
+ conn = get_connection()
+ cursor = conn.cursor()
+
+ try:
+ # Get daily summary
+ cursor.execute("""
+ SELECT * FROM daily_recommendations WHERE date = ?
+ """, (date,))
+ row = cursor.fetchone()
+
+ if not row:
+ return None
+
+ # Get stock analysis for this date
+ cursor.execute("""
+ SELECT * FROM stock_analysis WHERE date = ?
+ """, (date,))
+ analysis_rows = cursor.fetchall()
+
+ analysis = {}
+ for a in analysis_rows:
+ analysis[a['symbol']] = {
+ 'symbol': a['symbol'],
+ 'company_name': a['company_name'],
+ 'decision': a['decision'],
+ 'confidence': a['confidence'],
+ 'risk': a['risk'],
+ 'raw_analysis': a['raw_analysis']
+ }
+
+ return {
+ 'date': row['date'],
+ 'analysis': analysis,
+ 'summary': {
+ 'total': row['summary_total'],
+ 'buy': row['summary_buy'],
+ 'sell': row['summary_sell'],
+ 'hold': row['summary_hold']
+ },
+ 'top_picks': json.loads(row['top_picks']) if row['top_picks'] else [],
+ 'stocks_to_avoid': json.loads(row['stocks_to_avoid']) if row['stocks_to_avoid'] else []
+ }
+ finally:
+ conn.close()
+
+
+def get_latest_recommendation() -> Optional[dict]:
+ """Get the most recent recommendation."""
+ conn = get_connection()
+ cursor = conn.cursor()
+
+ try:
+ cursor.execute("""
+ SELECT date FROM daily_recommendations ORDER BY date DESC LIMIT 1
+ """)
+ row = cursor.fetchone()
+
+ if not row:
+ return None
+
+ return get_recommendation_by_date(row['date'])
+ finally:
+ conn.close()
+
+
+def get_all_dates() -> list:
+ """Get all available dates."""
+ conn = get_connection()
+ cursor = conn.cursor()
+
+ try:
+ cursor.execute("""
+ SELECT date FROM daily_recommendations ORDER BY date DESC
+ """)
+ return [row['date'] for row in cursor.fetchall()]
+ finally:
+ conn.close()
+
+
+def get_stock_history(symbol: str) -> list:
+ """Get historical recommendations for a specific stock."""
+ conn = get_connection()
+ cursor = conn.cursor()
+
+ try:
+ cursor.execute("""
+ SELECT date, decision, confidence, risk
+ FROM stock_analysis
+ WHERE symbol = ?
+ ORDER BY date DESC
+ """, (symbol,))
+
+ return [
+ {
+ 'date': row['date'],
+ 'decision': row['decision'],
+ 'confidence': row['confidence'],
+ 'risk': row['risk']
+ }
+ for row in cursor.fetchall()
+ ]
+ finally:
+ conn.close()
+
+
+def get_all_recommendations() -> list:
+ """Get all daily recommendations."""
+ dates = get_all_dates()
+ return [get_recommendation_by_date(date) for date in dates]
+
+
+# Initialize database on module import
+init_db()
diff --git a/frontend/backend/recommendations.db b/frontend/backend/recommendations.db
new file mode 100644
index 00000000..dd4d58bb
Binary files /dev/null and b/frontend/backend/recommendations.db differ
diff --git a/frontend/backend/requirements.txt b/frontend/backend/requirements.txt
new file mode 100644
index 00000000..57e2622f
--- /dev/null
+++ b/frontend/backend/requirements.txt
@@ -0,0 +1,3 @@
+fastapi>=0.109.0
+uvicorn>=0.27.0
+pydantic>=2.0.0
diff --git a/frontend/backend/seed_data.py b/frontend/backend/seed_data.py
new file mode 100644
index 00000000..9c631849
--- /dev/null
+++ b/frontend/backend/seed_data.py
@@ -0,0 +1,135 @@
+"""Seed the database with sample data from the Jan 30, 2025 analysis."""
+import database as db
+
+# Sample data from the Jan 30, 2025 analysis
+SAMPLE_DATA = {
+ "date": "2025-01-30",
+ "analysis": {
+ "RELIANCE": {"symbol": "RELIANCE", "company_name": "Reliance Industries Ltd", "decision": "HOLD", "confidence": "MEDIUM", "risk": "MEDIUM"},
+ "TCS": {"symbol": "TCS", "company_name": "Tata Consultancy Services Ltd", "decision": "HOLD", "confidence": "MEDIUM", "risk": "MEDIUM"},
+ "HDFCBANK": {"symbol": "HDFCBANK", "company_name": "HDFC Bank Ltd", "decision": "HOLD", "confidence": "MEDIUM", "risk": "MEDIUM"},
+ "INFY": {"symbol": "INFY", "company_name": "Infosys Ltd", "decision": "HOLD", "confidence": "MEDIUM", "risk": "MEDIUM"},
+ "ICICIBANK": {"symbol": "ICICIBANK", "company_name": "ICICI Bank Ltd", "decision": "BUY", "confidence": "MEDIUM", "risk": "MEDIUM"},
+ "HINDUNILVR": {"symbol": "HINDUNILVR", "company_name": "Hindustan Unilever Ltd", "decision": "HOLD", "confidence": "MEDIUM", "risk": "MEDIUM"},
+ "ITC": {"symbol": "ITC", "company_name": "ITC Ltd", "decision": "HOLD", "confidence": "MEDIUM", "risk": "MEDIUM"},
+ "SBIN": {"symbol": "SBIN", "company_name": "State Bank of India", "decision": "HOLD", "confidence": "MEDIUM", "risk": "MEDIUM"},
+ "BHARTIARTL": {"symbol": "BHARTIARTL", "company_name": "Bharti Airtel Ltd", "decision": "HOLD", "confidence": "MEDIUM", "risk": "MEDIUM"},
+ "KOTAKBANK": {"symbol": "KOTAKBANK", "company_name": "Kotak Mahindra Bank Ltd", "decision": "BUY", "confidence": "MEDIUM", "risk": "MEDIUM"},
+ "LT": {"symbol": "LT", "company_name": "Larsen & Toubro Ltd", "decision": "HOLD", "confidence": "MEDIUM", "risk": "MEDIUM"},
+ "AXISBANK": {"symbol": "AXISBANK", "company_name": "Axis Bank Ltd", "decision": "SELL", "confidence": "HIGH", "risk": "HIGH"},
+ "ASIANPAINT": {"symbol": "ASIANPAINT", "company_name": "Asian Paints Ltd", "decision": "HOLD", "confidence": "MEDIUM", "risk": "MEDIUM"},
+ "MARUTI": {"symbol": "MARUTI", "company_name": "Maruti Suzuki India Ltd", "decision": "HOLD", "confidence": "MEDIUM", "risk": "MEDIUM"},
+ "HCLTECH": {"symbol": "HCLTECH", "company_name": "HCL Technologies Ltd", "decision": "SELL", "confidence": "MEDIUM", "risk": "HIGH"},
+ "SUNPHARMA": {"symbol": "SUNPHARMA", "company_name": "Sun Pharmaceutical Industries Ltd", "decision": "SELL", "confidence": "MEDIUM", "risk": "MEDIUM"},
+ "TITAN": {"symbol": "TITAN", "company_name": "Titan Company Ltd", "decision": "HOLD", "confidence": "MEDIUM", "risk": "MEDIUM"},
+ "BAJFINANCE": {"symbol": "BAJFINANCE", "company_name": "Bajaj Finance Ltd", "decision": "BUY", "confidence": "HIGH", "risk": "MEDIUM"},
+ "WIPRO": {"symbol": "WIPRO", "company_name": "Wipro Ltd", "decision": "HOLD", "confidence": "MEDIUM", "risk": "MEDIUM"},
+ "ULTRACEMCO": {"symbol": "ULTRACEMCO", "company_name": "UltraTech Cement Ltd", "decision": "BUY", "confidence": "MEDIUM", "risk": "MEDIUM"},
+ "NESTLEIND": {"symbol": "NESTLEIND", "company_name": "Nestle India Ltd", "decision": "HOLD", "confidence": "MEDIUM", "risk": "MEDIUM"},
+ "NTPC": {"symbol": "NTPC", "company_name": "NTPC Ltd", "decision": "HOLD", "confidence": "MEDIUM", "risk": "MEDIUM"},
+ "POWERGRID": {"symbol": "POWERGRID", "company_name": "Power Grid Corporation of India Ltd", "decision": "SELL", "confidence": "MEDIUM", "risk": "MEDIUM"},
+ "M&M": {"symbol": "M&M", "company_name": "Mahindra & Mahindra Ltd", "decision": "HOLD", "confidence": "MEDIUM", "risk": "MEDIUM"},
+ "TATAMOTORS": {"symbol": "TATAMOTORS", "company_name": "Tata Motors Ltd", "decision": "HOLD", "confidence": "MEDIUM", "risk": "MEDIUM"},
+ "ONGC": {"symbol": "ONGC", "company_name": "Oil & Natural Gas Corporation Ltd", "decision": "SELL", "confidence": "MEDIUM", "risk": "HIGH"},
+ "JSWSTEEL": {"symbol": "JSWSTEEL", "company_name": "JSW Steel Ltd", "decision": "BUY", "confidence": "MEDIUM", "risk": "MEDIUM"},
+ "TATASTEEL": {"symbol": "TATASTEEL", "company_name": "Tata Steel Ltd", "decision": "HOLD", "confidence": "MEDIUM", "risk": "MEDIUM"},
+ "ADANIENT": {"symbol": "ADANIENT", "company_name": "Adani Enterprises Ltd", "decision": "HOLD", "confidence": "LOW", "risk": "HIGH"},
+ "ADANIPORTS": {"symbol": "ADANIPORTS", "company_name": "Adani Ports and SEZ Ltd", "decision": "SELL", "confidence": "MEDIUM", "risk": "HIGH"},
+ "COALINDIA": {"symbol": "COALINDIA", "company_name": "Coal India Ltd", "decision": "HOLD", "confidence": "MEDIUM", "risk": "MEDIUM"},
+ "BAJAJFINSV": {"symbol": "BAJAJFINSV", "company_name": "Bajaj Finserv Ltd", "decision": "BUY", "confidence": "HIGH", "risk": "MEDIUM"},
+ "TECHM": {"symbol": "TECHM", "company_name": "Tech Mahindra Ltd", "decision": "HOLD", "confidence": "MEDIUM", "risk": "MEDIUM"},
+ "HDFCLIFE": {"symbol": "HDFCLIFE", "company_name": "HDFC Life Insurance Company Ltd", "decision": "HOLD", "confidence": "MEDIUM", "risk": "MEDIUM"},
+ "SBILIFE": {"symbol": "SBILIFE", "company_name": "SBI Life Insurance Company Ltd", "decision": "HOLD", "confidence": "MEDIUM", "risk": "MEDIUM"},
+ "GRASIM": {"symbol": "GRASIM", "company_name": "Grasim Industries Ltd", "decision": "HOLD", "confidence": "MEDIUM", "risk": "MEDIUM"},
+ "DIVISLAB": {"symbol": "DIVISLAB", "company_name": "Divi's Laboratories Ltd", "decision": "SELL", "confidence": "MEDIUM", "risk": "MEDIUM"},
+ "DRREDDY": {"symbol": "DRREDDY", "company_name": "Dr. Reddy's Laboratories Ltd", "decision": "SELL", "confidence": "HIGH", "risk": "HIGH"},
+ "CIPLA": {"symbol": "CIPLA", "company_name": "Cipla Ltd", "decision": "HOLD", "confidence": "MEDIUM", "risk": "MEDIUM"},
+ "BRITANNIA": {"symbol": "BRITANNIA", "company_name": "Britannia Industries Ltd", "decision": "BUY", "confidence": "MEDIUM", "risk": "LOW"},
+ "EICHERMOT": {"symbol": "EICHERMOT", "company_name": "Eicher Motors Ltd", "decision": "HOLD", "confidence": "MEDIUM", "risk": "MEDIUM"},
+ "APOLLOHOSP": {"symbol": "APOLLOHOSP", "company_name": "Apollo Hospitals Enterprise Ltd", "decision": "HOLD", "confidence": "MEDIUM", "risk": "MEDIUM"},
+ "INDUSINDBK": {"symbol": "INDUSINDBK", "company_name": "IndusInd Bank Ltd", "decision": "SELL", "confidence": "HIGH", "risk": "HIGH"},
+ "HEROMOTOCO": {"symbol": "HEROMOTOCO", "company_name": "Hero MotoCorp Ltd", "decision": "HOLD", "confidence": "MEDIUM", "risk": "MEDIUM"},
+ "TATACONSUM": {"symbol": "TATACONSUM", "company_name": "Tata Consumer Products Ltd", "decision": "HOLD", "confidence": "MEDIUM", "risk": "MEDIUM"},
+ "BPCL": {"symbol": "BPCL", "company_name": "Bharat Petroleum Corporation Ltd", "decision": "SELL", "confidence": "MEDIUM", "risk": "MEDIUM"},
+ "UPL": {"symbol": "UPL", "company_name": "UPL Ltd", "decision": "HOLD", "confidence": "LOW", "risk": "HIGH"},
+ "HINDALCO": {"symbol": "HINDALCO", "company_name": "Hindalco Industries Ltd", "decision": "HOLD", "confidence": "MEDIUM", "risk": "MEDIUM"},
+ "BAJAJ-AUTO": {"symbol": "BAJAJ-AUTO", "company_name": "Bajaj Auto Ltd", "decision": "HOLD", "confidence": "MEDIUM", "risk": "MEDIUM"},
+ "LTIM": {"symbol": "LTIM", "company_name": "LTIMindtree Ltd", "decision": "HOLD", "confidence": "MEDIUM", "risk": "MEDIUM"},
+ },
+ "summary": {
+ "total": 50,
+ "buy": 7,
+ "sell": 10,
+ "hold": 33,
+ },
+ "top_picks": [
+ {
+ "rank": 1,
+ "symbol": "BAJFINANCE",
+ "company_name": "Bajaj Finance Ltd",
+ "decision": "BUY",
+ "reason": "13.7% gain over 30 days (Rs.678 to Rs.771), strongest bullish momentum with robust upward trend.",
+ "risk_level": "MEDIUM",
+ },
+ {
+ "rank": 2,
+ "symbol": "BAJAJFINSV",
+ "company_name": "Bajaj Finserv Ltd",
+ "decision": "BUY",
+ "reason": "14% gain in one month (Rs.1,567 to Rs.1,789) demonstrates clear bullish momentum with sector-wide tailwinds.",
+ "risk_level": "MEDIUM",
+ },
+ {
+ "rank": 3,
+ "symbol": "KOTAKBANK",
+ "company_name": "Kotak Mahindra Bank Ltd",
+ "decision": "BUY",
+ "reason": "Significant breakout on January 20th with 9.2% gain on exceptionally high volume (66.6M shares).",
+ "risk_level": "MEDIUM",
+ },
+ ],
+ "stocks_to_avoid": [
+ {
+ "symbol": "DRREDDY",
+ "company_name": "Dr. Reddy's Laboratories Ltd",
+ "reason": "HIGH CONFIDENCE SELL with 14.9% decline in one month. Severe downtrend with high risk.",
+ },
+ {
+ "symbol": "AXISBANK",
+ "company_name": "Axis Bank Ltd",
+ "reason": "HIGH CONFIDENCE SELL with 10.5% sustained decline. Clear and persistent downtrend.",
+ },
+ {
+ "symbol": "HCLTECH",
+ "company_name": "HCL Technologies Ltd",
+ "reason": "SELL with 9.4% drop from recent highs. High risk rating with continued selling pressure.",
+ },
+ {
+ "symbol": "ADANIPORTS",
+ "company_name": "Adani Ports and SEZ Ltd",
+ "reason": "SELL with 12% monthly decline and consistently lower lows. High risk profile.",
+ },
+ ],
+}
+
+
+def seed_database():
+ """Seed the database with sample data."""
+ print("Seeding database...")
+
+ db.save_recommendation(
+ date=SAMPLE_DATA["date"],
+ analysis_data=SAMPLE_DATA["analysis"],
+ summary=SAMPLE_DATA["summary"],
+ top_picks=SAMPLE_DATA["top_picks"],
+ stocks_to_avoid=SAMPLE_DATA["stocks_to_avoid"],
+ )
+
+ print(f"Saved recommendation for {SAMPLE_DATA['date']}")
+ print(f" - {len(SAMPLE_DATA['analysis'])} stocks analyzed")
+ print(f" - Summary: {SAMPLE_DATA['summary']['buy']} BUY, {SAMPLE_DATA['summary']['sell']} SELL, {SAMPLE_DATA['summary']['hold']} HOLD")
+ print("Database seeded successfully!")
+
+
+if __name__ == "__main__":
+ seed_database()
diff --git a/frontend/backend/server.py b/frontend/backend/server.py
new file mode 100644
index 00000000..4a7870ee
--- /dev/null
+++ b/frontend/backend/server.py
@@ -0,0 +1,151 @@
+"""FastAPI server for Nifty50 AI recommendations."""
+from fastapi import FastAPI, HTTPException
+from fastapi.middleware.cors import CORSMiddleware
+from pydantic import BaseModel
+from typing import Optional
+import database as db
+
+app = FastAPI(
+ title="Nifty50 AI API",
+ description="API for Nifty 50 stock recommendations",
+ version="1.0.0"
+)
+
+# Enable CORS for frontend
+app.add_middleware(
+ CORSMiddleware,
+ allow_origins=["*"], # In production, replace with specific origins
+ allow_credentials=True,
+ allow_methods=["*"],
+ allow_headers=["*"],
+)
+
+
+class StockAnalysis(BaseModel):
+ symbol: str
+ company_name: str
+ decision: Optional[str] = None
+ confidence: Optional[str] = None
+ risk: Optional[str] = None
+ raw_analysis: Optional[str] = None
+
+
+class TopPick(BaseModel):
+ rank: int
+ symbol: str
+ company_name: str
+ decision: str
+ reason: str
+ risk_level: str
+
+
+class StockToAvoid(BaseModel):
+ symbol: str
+ company_name: str
+ reason: str
+
+
+class Summary(BaseModel):
+ total: int
+ buy: int
+ sell: int
+ hold: int
+
+
+class DailyRecommendation(BaseModel):
+ date: str
+ analysis: dict[str, StockAnalysis]
+ summary: Summary
+ top_picks: list[TopPick]
+ stocks_to_avoid: list[StockToAvoid]
+
+
+class SaveRecommendationRequest(BaseModel):
+ date: str
+ analysis: dict
+ summary: dict
+ top_picks: list
+ stocks_to_avoid: list
+
+
+@app.get("/")
+async def root():
+ """API root endpoint."""
+ return {
+ "name": "Nifty50 AI API",
+ "version": "1.0.0",
+ "endpoints": {
+ "GET /recommendations": "Get all recommendations",
+ "GET /recommendations/latest": "Get latest recommendation",
+ "GET /recommendations/{date}": "Get recommendation by date",
+ "GET /stocks/{symbol}/history": "Get stock history",
+ "GET /dates": "Get all available dates",
+ "POST /recommendations": "Save a new recommendation"
+ }
+ }
+
+
+@app.get("/recommendations")
+async def get_all_recommendations():
+ """Get all daily recommendations."""
+ recommendations = db.get_all_recommendations()
+ return {"recommendations": recommendations, "count": len(recommendations)}
+
+
+@app.get("/recommendations/latest")
+async def get_latest_recommendation():
+ """Get the most recent recommendation."""
+ recommendation = db.get_latest_recommendation()
+ if not recommendation:
+ raise HTTPException(status_code=404, detail="No recommendations found")
+ return recommendation
+
+
+@app.get("/recommendations/{date}")
+async def get_recommendation_by_date(date: str):
+ """Get recommendation for a specific date (format: YYYY-MM-DD)."""
+ recommendation = db.get_recommendation_by_date(date)
+ if not recommendation:
+ raise HTTPException(status_code=404, detail=f"No recommendation found for {date}")
+ return recommendation
+
+
+@app.get("/stocks/{symbol}/history")
+async def get_stock_history(symbol: str):
+ """Get historical recommendations for a specific stock."""
+ history = db.get_stock_history(symbol.upper())
+ return {"symbol": symbol.upper(), "history": history, "count": len(history)}
+
+
+@app.get("/dates")
+async def get_available_dates():
+ """Get all dates with recommendations."""
+ dates = db.get_all_dates()
+ return {"dates": dates, "count": len(dates)}
+
+
+@app.post("/recommendations")
+async def save_recommendation(request: SaveRecommendationRequest):
+ """Save a new daily recommendation."""
+ try:
+ db.save_recommendation(
+ date=request.date,
+ analysis_data=request.analysis,
+ summary=request.summary,
+ top_picks=request.top_picks,
+ stocks_to_avoid=request.stocks_to_avoid
+ )
+ return {"message": f"Recommendation for {request.date} saved successfully"}
+ except Exception as e:
+ raise HTTPException(status_code=500, detail=str(e))
+
+
+@app.get("/health")
+async def health_check():
+ """Health check endpoint."""
+ return {"status": "healthy", "database": "connected"}
+
+
+if __name__ == "__main__":
+ import uvicorn
+ uvicorn.run(app, host="0.0.0.0", port=8000)
diff --git a/frontend/eslint.config.js b/frontend/eslint.config.js
new file mode 100644
index 00000000..5e6b472f
--- /dev/null
+++ b/frontend/eslint.config.js
@@ -0,0 +1,23 @@
+import js from '@eslint/js'
+import globals from 'globals'
+import reactHooks from 'eslint-plugin-react-hooks'
+import reactRefresh from 'eslint-plugin-react-refresh'
+import tseslint from 'typescript-eslint'
+import { defineConfig, globalIgnores } from 'eslint/config'
+
+export default defineConfig([
+ globalIgnores(['dist']),
+ {
+ files: ['**/*.{ts,tsx}'],
+ extends: [
+ js.configs.recommended,
+ tseslint.configs.recommended,
+ reactHooks.configs.flat.recommended,
+ reactRefresh.configs.vite,
+ ],
+ languageOptions: {
+ ecmaVersion: 2020,
+ globals: globals.browser,
+ },
+ },
+])
diff --git a/frontend/index.html b/frontend/index.html
new file mode 100644
index 00000000..5a738c33
--- /dev/null
+++ b/frontend/index.html
@@ -0,0 +1,86 @@
+
+
+
+
+
+
+
+ Nifty50 AI - Daily Stock Recommendations for Indian Markets
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Nifty50 AI - Stock Recommendations
+
Please enable JavaScript to view this website.
+
+
+
+
diff --git a/frontend/package.json b/frontend/package.json
new file mode 100644
index 00000000..9ac3666d
--- /dev/null
+++ b/frontend/package.json
@@ -0,0 +1,39 @@
+{
+ "name": "frontend",
+ "private": true,
+ "version": "0.0.0",
+ "type": "module",
+ "scripts": {
+ "dev": "vite",
+ "build": "tsc -b && vite build",
+ "lint": "eslint .",
+ "preview": "vite preview"
+ },
+ "dependencies": {
+ "@tailwindcss/postcss": "^4.1.18",
+ "date-fns": "^4.1.0",
+ "lucide-react": "^0.563.0",
+ "react": "^19.2.0",
+ "react-dom": "^19.2.0",
+ "react-router-dom": "^7.13.0",
+ "recharts": "^3.7.0"
+ },
+ "devDependencies": {
+ "@eslint/js": "^9.39.1",
+ "@tailwindcss/vite": "^4.1.18",
+ "@types/node": "^24.10.1",
+ "@types/react": "^19.2.5",
+ "@types/react-dom": "^19.2.3",
+ "@vitejs/plugin-react": "^5.1.1",
+ "autoprefixer": "^10.4.24",
+ "eslint": "^9.39.1",
+ "eslint-plugin-react-hooks": "^7.0.1",
+ "eslint-plugin-react-refresh": "^0.4.24",
+ "globals": "^16.5.0",
+ "postcss": "^8.5.6",
+ "tailwindcss": "^4.1.18",
+ "typescript": "~5.9.3",
+ "typescript-eslint": "^8.46.4",
+ "vite": "^7.2.4"
+ }
+}
diff --git a/frontend/postcss.config.js b/frontend/postcss.config.js
new file mode 100644
index 00000000..1c878468
--- /dev/null
+++ b/frontend/postcss.config.js
@@ -0,0 +1,6 @@
+export default {
+ plugins: {
+ '@tailwindcss/postcss': {},
+ autoprefixer: {},
+ },
+}
diff --git a/frontend/public/favicon.svg b/frontend/public/favicon.svg
new file mode 100644
index 00000000..45c3cb4c
--- /dev/null
+++ b/frontend/public/favicon.svg
@@ -0,0 +1,11 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/frontend/public/vite.svg b/frontend/public/vite.svg
new file mode 100644
index 00000000..e7b8dfb1
--- /dev/null
+++ b/frontend/public/vite.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/frontend/src/App.css b/frontend/src/App.css
new file mode 100644
index 00000000..b9d355df
--- /dev/null
+++ b/frontend/src/App.css
@@ -0,0 +1,42 @@
+#root {
+ max-width: 1280px;
+ margin: 0 auto;
+ padding: 2rem;
+ text-align: center;
+}
+
+.logo {
+ height: 6em;
+ padding: 1.5em;
+ will-change: filter;
+ transition: filter 300ms;
+}
+.logo:hover {
+ filter: drop-shadow(0 0 2em #646cffaa);
+}
+.logo.react:hover {
+ filter: drop-shadow(0 0 2em #61dafbaa);
+}
+
+@keyframes logo-spin {
+ from {
+ transform: rotate(0deg);
+ }
+ to {
+ transform: rotate(360deg);
+ }
+}
+
+@media (prefers-reduced-motion: no-preference) {
+ a:nth-of-type(2) .logo {
+ animation: logo-spin infinite 20s linear;
+ }
+}
+
+.card {
+ padding: 2em;
+}
+
+.read-the-docs {
+ color: #888;
+}
diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx
new file mode 100644
index 00000000..5748cc6f
--- /dev/null
+++ b/frontend/src/App.tsx
@@ -0,0 +1,26 @@
+import { Routes, Route } from 'react-router-dom';
+import Header from './components/Header';
+import Footer from './components/Footer';
+import Dashboard from './pages/Dashboard';
+import History from './pages/History';
+import Stocks from './pages/Stocks';
+import StockDetail from './pages/StockDetail';
+
+function App() {
+ return (
+
+
+
+
+ } />
+ } />
+ } />
+ } />
+
+
+
+
+ );
+}
+
+export default App;
diff --git a/frontend/src/assets/react.svg b/frontend/src/assets/react.svg
new file mode 100644
index 00000000..6c87de9b
--- /dev/null
+++ b/frontend/src/assets/react.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/frontend/src/components/Charts.tsx b/frontend/src/components/Charts.tsx
new file mode 100644
index 00000000..98415f90
--- /dev/null
+++ b/frontend/src/components/Charts.tsx
@@ -0,0 +1,219 @@
+import { PieChart, Pie, Cell, ResponsiveContainer, Legend, Tooltip, BarChart, Bar, XAxis, YAxis, CartesianGrid } from 'recharts';
+
+interface SummaryChartProps {
+ buy: number;
+ sell: number;
+ hold: number;
+}
+
+const COLORS = {
+ buy: '#22c55e',
+ sell: '#ef4444',
+ hold: '#f59e0b',
+};
+
+export function SummaryPieChart({ buy, sell, hold }: SummaryChartProps) {
+ const data = [
+ { name: 'Buy', value: buy, color: COLORS.buy },
+ { name: 'Hold', value: hold, color: COLORS.hold },
+ { name: 'Sell', value: sell, color: COLORS.sell },
+ ];
+
+ return (
+
+
+
+ `${name} ${((percent ?? 0) * 100).toFixed(0)}%`}
+ labelLine={false}
+ >
+ {data.map((entry, index) => (
+ |
+ ))}
+
+ [`${value} stocks`, '']}
+ />
+ {value} }
+ />
+
+
+
+ );
+}
+
+interface HistoricalDataPoint {
+ date: string;
+ buy: number;
+ sell: number;
+ hold: number;
+}
+
+interface HistoricalChartProps {
+ data: HistoricalDataPoint[];
+}
+
+export function HistoricalBarChart({ data }: HistoricalChartProps) {
+ const formattedData = data.map(d => ({
+ ...d,
+ date: new Date(d.date).toLocaleDateString('en-IN', { month: 'short', day: 'numeric' }),
+ }));
+
+ return (
+
+
+
+
+
+
+
+ {value} }
+ />
+
+
+
+
+
+
+ );
+}
+
+interface StockHistoryEntry {
+ date: string;
+ decision: string;
+}
+
+interface StockHistoryChartProps {
+ history: StockHistoryEntry[];
+ symbol: string;
+}
+
+export function StockHistoryTimeline({ history, symbol }: StockHistoryChartProps) {
+ if (history.length === 0) {
+ return (
+
+ No historical data available for {symbol}
+
+ );
+ }
+
+ return (
+
+ {history.map((entry, idx) => {
+ const bgColor = entry.decision === 'BUY' ? 'bg-green-500' :
+ entry.decision === 'SELL' ? 'bg-red-500' : 'bg-amber-500';
+ const textColor = entry.decision === 'BUY' ? 'text-green-700' :
+ entry.decision === 'SELL' ? 'text-red-700' : 'text-amber-700';
+
+ return (
+
+
+ {new Date(entry.date).toLocaleDateString('en-IN', { month: 'short', day: 'numeric' })}
+
+
+
+ {entry.decision}
+
+
+ );
+ })}
+
+ );
+}
+
+interface DecisionDistributionProps {
+ total: number;
+ buy: number;
+ sell: number;
+ hold: number;
+}
+
+export function DecisionDistribution({ total, buy, sell, hold }: DecisionDistributionProps) {
+ const buyPercent = ((buy / total) * 100).toFixed(1);
+ const sellPercent = ((sell / total) * 100).toFixed(1);
+ const holdPercent = ((hold / total) * 100).toFixed(1);
+
+ return (
+
+
+
+
+
+
+
{buy}
+
{buyPercent}%
+
+
+
+
+
{hold}
+
{holdPercent}%
+
+
+
+
+
{sell}
+
{sellPercent}%
+
+
+
+ );
+}
diff --git a/frontend/src/components/Footer.tsx b/frontend/src/components/Footer.tsx
new file mode 100644
index 00000000..a2808037
--- /dev/null
+++ b/frontend/src/components/Footer.tsx
@@ -0,0 +1,69 @@
+import { TrendingUp, Github, Twitter } from 'lucide-react';
+
+export default function Footer() {
+ return (
+
+
+
+ {/* Brand */}
+
+
+
+ AI-powered stock recommendations for Nifty 50 stocks. Using advanced machine learning
+ to analyze market trends, technical indicators, and news sentiment.
+
+
+
+ {/* Quick Links */}
+
+
+ {/* Legal */}
+
+
+
+
+
+ © {new Date().getFullYear()} Nifty50 AI. All rights reserved.
+
+
+
+
+ {/* Disclaimer */}
+
+
+ Disclaimer: This website provides AI-generated stock recommendations for
+ educational purposes only. These are not financial advice. Always do your own research
+ and consult with a qualified financial advisor before making investment decisions.
+ Past performance does not guarantee future results.
+
+
+
+
+ );
+}
diff --git a/frontend/src/components/Header.tsx b/frontend/src/components/Header.tsx
new file mode 100644
index 00000000..0f7e4b24
--- /dev/null
+++ b/frontend/src/components/Header.tsx
@@ -0,0 +1,82 @@
+import { Link, useLocation } from 'react-router-dom';
+import { TrendingUp, BarChart3, History, Menu, X } from 'lucide-react';
+import { useState } from 'react';
+
+export default function Header() {
+ const location = useLocation();
+ const [mobileMenuOpen, setMobileMenuOpen] = useState(false);
+
+ const navItems = [
+ { path: '/', label: 'Dashboard', icon: BarChart3 },
+ { path: '/history', label: 'History', icon: History },
+ { path: '/stocks', label: 'All Stocks', icon: TrendingUp },
+ ];
+
+ const isActive = (path: string) => location.pathname === path;
+
+ return (
+
+ );
+}
diff --git a/frontend/src/components/StockCard.tsx b/frontend/src/components/StockCard.tsx
new file mode 100644
index 00000000..f24267ab
--- /dev/null
+++ b/frontend/src/components/StockCard.tsx
@@ -0,0 +1,117 @@
+import { Link } from 'react-router-dom';
+import { TrendingUp, TrendingDown, Minus, ChevronRight } from 'lucide-react';
+import type { StockAnalysis, Decision } from '../types';
+
+interface StockCardProps {
+ stock: StockAnalysis;
+ showDetails?: boolean;
+}
+
+export function DecisionBadge({ decision }: { decision: Decision | null }) {
+ if (!decision) return null;
+
+ const config = {
+ BUY: {
+ bg: 'bg-green-100',
+ text: 'text-green-800',
+ icon: TrendingUp,
+ },
+ SELL: {
+ bg: 'bg-red-100',
+ text: 'text-red-800',
+ icon: TrendingDown,
+ },
+ HOLD: {
+ bg: 'bg-amber-100',
+ text: 'text-amber-800',
+ icon: Minus,
+ },
+ };
+
+ const { bg, text, icon: Icon } = config[decision];
+
+ return (
+
+
+ {decision}
+
+ );
+}
+
+export function ConfidenceBadge({ confidence }: { confidence?: string }) {
+ if (!confidence) return null;
+
+ const colors = {
+ HIGH: 'bg-green-50 text-green-700 border-green-200',
+ MEDIUM: 'bg-amber-50 text-amber-700 border-amber-200',
+ LOW: 'bg-gray-50 text-gray-700 border-gray-200',
+ };
+
+ return (
+
+ {confidence} Confidence
+
+ );
+}
+
+export function RiskBadge({ risk }: { risk?: string }) {
+ if (!risk) return null;
+
+ const colors = {
+ HIGH: 'text-red-600',
+ MEDIUM: 'text-amber-600',
+ LOW: 'text-green-600',
+ };
+
+ return (
+
+ {risk} Risk
+
+ );
+}
+
+export default function StockCard({ stock, showDetails = true }: StockCardProps) {
+ return (
+
+
+
+
{stock.symbol}
+
+
+
{stock.company_name}
+ {showDetails && (
+
+
+
+
+ )}
+
+
+
+ );
+}
+
+export function StockCardCompact({ stock }: { stock: StockAnalysis }) {
+ return (
+
+
+
+
+ {stock.symbol}
+ ·
+ {stock.company_name}
+
+
+
+
+ );
+}
diff --git a/frontend/src/components/SummaryStats.tsx b/frontend/src/components/SummaryStats.tsx
new file mode 100644
index 00000000..6a68afc7
--- /dev/null
+++ b/frontend/src/components/SummaryStats.tsx
@@ -0,0 +1,99 @@
+import { TrendingUp, TrendingDown, Minus, BarChart2 } from 'lucide-react';
+
+interface SummaryStatsProps {
+ total: number;
+ buy: number;
+ sell: number;
+ hold: number;
+ date: string;
+}
+
+export default function SummaryStats({ total, buy, sell, hold, date }: SummaryStatsProps) {
+ const stats = [
+ {
+ label: 'Total Analyzed',
+ value: total,
+ icon: BarChart2,
+ color: 'text-nifty-600',
+ bg: 'bg-nifty-50',
+ },
+ {
+ label: 'Buy',
+ value: buy,
+ icon: TrendingUp,
+ color: 'text-green-600',
+ bg: 'bg-green-50',
+ percentage: ((buy / total) * 100).toFixed(0),
+ },
+ {
+ label: 'Sell',
+ value: sell,
+ icon: TrendingDown,
+ color: 'text-red-600',
+ bg: 'bg-red-50',
+ percentage: ((sell / total) * 100).toFixed(0),
+ },
+ {
+ label: 'Hold',
+ value: hold,
+ icon: Minus,
+ color: 'text-amber-600',
+ bg: 'bg-amber-50',
+ percentage: ((hold / total) * 100).toFixed(0),
+ },
+ ];
+
+ return (
+
+
+
Today's Summary
+
+ {new Date(date).toLocaleDateString('en-IN', {
+ weekday: 'long',
+ year: 'numeric',
+ month: 'long',
+ day: 'numeric',
+ })}
+
+
+
+
+ {stats.map(({ label, value, icon: Icon, color, bg, percentage }) => (
+
+
+
+ {percentage && (
+ {percentage}%
+ )}
+
+
{value}
+
{label}
+
+ ))}
+
+
+ {/* Progress bar */}
+
+
+
+ Buy ({buy})
+ Hold ({hold})
+ Sell ({sell})
+
+
+
+ );
+}
diff --git a/frontend/src/components/TopPicks.tsx b/frontend/src/components/TopPicks.tsx
new file mode 100644
index 00000000..08c934c6
--- /dev/null
+++ b/frontend/src/components/TopPicks.tsx
@@ -0,0 +1,96 @@
+import { Link } from 'react-router-dom';
+import { Trophy, TrendingUp, AlertTriangle, ChevronRight } from 'lucide-react';
+import type { TopPick, StockToAvoid } from '../types';
+
+interface TopPicksProps {
+ picks: TopPick[];
+}
+
+export default function TopPicks({ picks }: TopPicksProps) {
+ const medals = ['🥇', '🥈', '🥉'];
+ const bgColors = [
+ 'bg-gradient-to-br from-amber-50 to-yellow-50 border-amber-200',
+ 'bg-gradient-to-br from-gray-50 to-slate-50 border-gray-200',
+ 'bg-gradient-to-br from-orange-50 to-amber-50 border-orange-200',
+ ];
+
+ return (
+
+
+
+
Top Picks
+
+
+
+ {picks.map((pick, index) => (
+
+
+
+
{medals[index]}
+
+
+
{pick.symbol}
+
+
+ {pick.decision}
+
+
+
{pick.company_name}
+
+
+
+
+
{pick.reason}
+
+
+ {pick.risk_level} Risk
+
+
+
+ ))}
+
+
+ );
+}
+
+interface StocksToAvoidProps {
+ stocks: StockToAvoid[];
+}
+
+export function StocksToAvoid({ stocks }: StocksToAvoidProps) {
+ return (
+
+
+
+
+ {stocks.map((stock) => (
+
+
+
+ {stock.symbol}
+ SELL
+
+
+
+
{stock.reason}
+
+ ))}
+
+
+ );
+}
diff --git a/frontend/src/data/recommendations.ts b/frontend/src/data/recommendations.ts
new file mode 100644
index 00000000..7c1037de
--- /dev/null
+++ b/frontend/src/data/recommendations.ts
@@ -0,0 +1,152 @@
+import type { DailyRecommendation, Decision } from '../types';
+
+// Sample data from the Jan 30, 2025 analysis
+export const sampleRecommendations: DailyRecommendation[] = [
+ {
+ date: '2025-01-30',
+ analysis: {
+ 'RELIANCE': { symbol: 'RELIANCE', company_name: 'Reliance Industries Ltd', decision: 'HOLD', confidence: 'MEDIUM', risk: 'MEDIUM' },
+ 'TCS': { symbol: 'TCS', company_name: 'Tata Consultancy Services Ltd', decision: 'HOLD', confidence: 'MEDIUM', risk: 'MEDIUM' },
+ 'HDFCBANK': { symbol: 'HDFCBANK', company_name: 'HDFC Bank Ltd', decision: 'HOLD', confidence: 'MEDIUM', risk: 'MEDIUM' },
+ 'INFY': { symbol: 'INFY', company_name: 'Infosys Ltd', decision: 'HOLD', confidence: 'MEDIUM', risk: 'MEDIUM' },
+ 'ICICIBANK': { symbol: 'ICICIBANK', company_name: 'ICICI Bank Ltd', decision: 'BUY', confidence: 'MEDIUM', risk: 'MEDIUM' },
+ 'HINDUNILVR': { symbol: 'HINDUNILVR', company_name: 'Hindustan Unilever Ltd', decision: 'HOLD', confidence: 'MEDIUM', risk: 'MEDIUM' },
+ 'ITC': { symbol: 'ITC', company_name: 'ITC Ltd', decision: 'HOLD', confidence: 'MEDIUM', risk: 'MEDIUM' },
+ 'SBIN': { symbol: 'SBIN', company_name: 'State Bank of India', decision: 'HOLD', confidence: 'MEDIUM', risk: 'MEDIUM' },
+ 'BHARTIARTL': { symbol: 'BHARTIARTL', company_name: 'Bharti Airtel Ltd', decision: 'HOLD', confidence: 'MEDIUM', risk: 'MEDIUM' },
+ 'KOTAKBANK': { symbol: 'KOTAKBANK', company_name: 'Kotak Mahindra Bank Ltd', decision: 'BUY', confidence: 'MEDIUM', risk: 'MEDIUM' },
+ 'LT': { symbol: 'LT', company_name: 'Larsen & Toubro Ltd', decision: 'HOLD', confidence: 'MEDIUM', risk: 'MEDIUM' },
+ 'AXISBANK': { symbol: 'AXISBANK', company_name: 'Axis Bank Ltd', decision: 'SELL', confidence: 'HIGH', risk: 'HIGH' },
+ 'ASIANPAINT': { symbol: 'ASIANPAINT', company_name: 'Asian Paints Ltd', decision: 'HOLD', confidence: 'MEDIUM', risk: 'MEDIUM' },
+ 'MARUTI': { symbol: 'MARUTI', company_name: 'Maruti Suzuki India Ltd', decision: 'HOLD', confidence: 'MEDIUM', risk: 'MEDIUM' },
+ 'HCLTECH': { symbol: 'HCLTECH', company_name: 'HCL Technologies Ltd', decision: 'SELL', confidence: 'MEDIUM', risk: 'HIGH' },
+ 'SUNPHARMA': { symbol: 'SUNPHARMA', company_name: 'Sun Pharmaceutical Industries Ltd', decision: 'SELL', confidence: 'MEDIUM', risk: 'MEDIUM' },
+ 'TITAN': { symbol: 'TITAN', company_name: 'Titan Company Ltd', decision: 'HOLD', confidence: 'MEDIUM', risk: 'MEDIUM' },
+ 'BAJFINANCE': { symbol: 'BAJFINANCE', company_name: 'Bajaj Finance Ltd', decision: 'BUY', confidence: 'HIGH', risk: 'MEDIUM' },
+ 'WIPRO': { symbol: 'WIPRO', company_name: 'Wipro Ltd', decision: 'HOLD', confidence: 'MEDIUM', risk: 'MEDIUM' },
+ 'ULTRACEMCO': { symbol: 'ULTRACEMCO', company_name: 'UltraTech Cement Ltd', decision: 'BUY', confidence: 'MEDIUM', risk: 'MEDIUM' },
+ 'NESTLEIND': { symbol: 'NESTLEIND', company_name: 'Nestle India Ltd', decision: 'HOLD', confidence: 'MEDIUM', risk: 'MEDIUM' },
+ 'NTPC': { symbol: 'NTPC', company_name: 'NTPC Ltd', decision: 'HOLD', confidence: 'MEDIUM', risk: 'MEDIUM' },
+ 'POWERGRID': { symbol: 'POWERGRID', company_name: 'Power Grid Corporation of India Ltd', decision: 'SELL', confidence: 'MEDIUM', risk: 'MEDIUM' },
+ 'M&M': { symbol: 'M&M', company_name: 'Mahindra & Mahindra Ltd', decision: 'HOLD', confidence: 'MEDIUM', risk: 'MEDIUM' },
+ 'TATAMOTORS': { symbol: 'TATAMOTORS', company_name: 'Tata Motors Ltd', decision: 'HOLD', confidence: 'MEDIUM', risk: 'MEDIUM' },
+ 'ONGC': { symbol: 'ONGC', company_name: 'Oil & Natural Gas Corporation Ltd', decision: 'SELL', confidence: 'MEDIUM', risk: 'HIGH' },
+ 'JSWSTEEL': { symbol: 'JSWSTEEL', company_name: 'JSW Steel Ltd', decision: 'BUY', confidence: 'MEDIUM', risk: 'MEDIUM' },
+ 'TATASTEEL': { symbol: 'TATASTEEL', company_name: 'Tata Steel Ltd', decision: 'HOLD', confidence: 'MEDIUM', risk: 'MEDIUM' },
+ 'ADANIENT': { symbol: 'ADANIENT', company_name: 'Adani Enterprises Ltd', decision: 'HOLD', confidence: 'LOW', risk: 'HIGH' },
+ 'ADANIPORTS': { symbol: 'ADANIPORTS', company_name: 'Adani Ports and SEZ Ltd', decision: 'SELL', confidence: 'MEDIUM', risk: 'HIGH' },
+ 'COALINDIA': { symbol: 'COALINDIA', company_name: 'Coal India Ltd', decision: 'HOLD', confidence: 'MEDIUM', risk: 'MEDIUM' },
+ 'BAJAJFINSV': { symbol: 'BAJAJFINSV', company_name: 'Bajaj Finserv Ltd', decision: 'BUY', confidence: 'HIGH', risk: 'MEDIUM' },
+ 'TECHM': { symbol: 'TECHM', company_name: 'Tech Mahindra Ltd', decision: 'HOLD', confidence: 'MEDIUM', risk: 'MEDIUM' },
+ 'HDFCLIFE': { symbol: 'HDFCLIFE', company_name: 'HDFC Life Insurance Company Ltd', decision: 'HOLD', confidence: 'MEDIUM', risk: 'MEDIUM' },
+ 'SBILIFE': { symbol: 'SBILIFE', company_name: 'SBI Life Insurance Company Ltd', decision: 'HOLD', confidence: 'MEDIUM', risk: 'MEDIUM' },
+ 'GRASIM': { symbol: 'GRASIM', company_name: 'Grasim Industries Ltd', decision: 'HOLD', confidence: 'MEDIUM', risk: 'MEDIUM' },
+ 'DIVISLAB': { symbol: 'DIVISLAB', company_name: "Divi's Laboratories Ltd", decision: 'SELL', confidence: 'MEDIUM', risk: 'MEDIUM' },
+ 'DRREDDY': { symbol: 'DRREDDY', company_name: "Dr. Reddy's Laboratories Ltd", decision: 'SELL', confidence: 'HIGH', risk: 'HIGH' },
+ 'CIPLA': { symbol: 'CIPLA', company_name: 'Cipla Ltd', decision: 'HOLD', confidence: 'MEDIUM', risk: 'MEDIUM' },
+ 'BRITANNIA': { symbol: 'BRITANNIA', company_name: 'Britannia Industries Ltd', decision: 'BUY', confidence: 'MEDIUM', risk: 'LOW' },
+ 'EICHERMOT': { symbol: 'EICHERMOT', company_name: 'Eicher Motors Ltd', decision: 'HOLD', confidence: 'MEDIUM', risk: 'MEDIUM' },
+ 'APOLLOHOSP': { symbol: 'APOLLOHOSP', company_name: 'Apollo Hospitals Enterprise Ltd', decision: 'HOLD', confidence: 'MEDIUM', risk: 'MEDIUM' },
+ 'INDUSINDBK': { symbol: 'INDUSINDBK', company_name: 'IndusInd Bank Ltd', decision: 'SELL', confidence: 'HIGH', risk: 'HIGH' },
+ 'HEROMOTOCO': { symbol: 'HEROMOTOCO', company_name: 'Hero MotoCorp Ltd', decision: 'HOLD', confidence: 'MEDIUM', risk: 'MEDIUM' },
+ 'TATACONSUM': { symbol: 'TATACONSUM', company_name: 'Tata Consumer Products Ltd', decision: 'HOLD', confidence: 'MEDIUM', risk: 'MEDIUM' },
+ 'BPCL': { symbol: 'BPCL', company_name: 'Bharat Petroleum Corporation Ltd', decision: 'SELL', confidence: 'MEDIUM', risk: 'MEDIUM' },
+ 'UPL': { symbol: 'UPL', company_name: 'UPL Ltd', decision: 'HOLD', confidence: 'LOW', risk: 'HIGH' },
+ 'HINDALCO': { symbol: 'HINDALCO', company_name: 'Hindalco Industries Ltd', decision: 'HOLD', confidence: 'MEDIUM', risk: 'MEDIUM' },
+ 'BAJAJ-AUTO': { symbol: 'BAJAJ-AUTO', company_name: 'Bajaj Auto Ltd', decision: 'HOLD', confidence: 'MEDIUM', risk: 'MEDIUM' },
+ 'LTIM': { symbol: 'LTIM', company_name: 'LTIMindtree Ltd', decision: 'HOLD', confidence: 'MEDIUM', risk: 'MEDIUM' },
+ },
+ ranking: {
+ ranking: '',
+ stocks_analyzed: 50,
+ timestamp: '2025-01-30T15:30:00.000Z',
+ },
+ summary: {
+ total: 50,
+ buy: 7,
+ sell: 10,
+ hold: 33,
+ },
+ top_picks: [
+ {
+ rank: 1,
+ symbol: 'BAJFINANCE',
+ company_name: 'Bajaj Finance Ltd',
+ decision: 'BUY',
+ reason: '13.7% gain over 30 days (₹678 → ₹771), strongest bullish momentum with robust upward trend.',
+ risk_level: 'MEDIUM',
+ },
+ {
+ rank: 2,
+ symbol: 'BAJAJFINSV',
+ company_name: 'Bajaj Finserv Ltd',
+ decision: 'BUY',
+ reason: '14% gain in one month (₹1,567 → ₹1,789) demonstrates clear bullish momentum with sector-wide tailwinds.',
+ risk_level: 'MEDIUM',
+ },
+ {
+ rank: 3,
+ symbol: 'KOTAKBANK',
+ company_name: 'Kotak Mahindra Bank Ltd',
+ decision: 'BUY',
+ reason: 'Significant breakout on January 20th with 9.2% gain on exceptionally high volume (66.6M shares).',
+ risk_level: 'MEDIUM',
+ },
+ ],
+ stocks_to_avoid: [
+ {
+ symbol: 'DRREDDY',
+ company_name: "Dr. Reddy's Laboratories Ltd",
+ reason: 'HIGH CONFIDENCE SELL with 14.9% decline in one month. Severe downtrend with high risk.',
+ },
+ {
+ symbol: 'AXISBANK',
+ company_name: 'Axis Bank Ltd',
+ reason: 'HIGH CONFIDENCE SELL with 10.5% sustained decline. Clear and persistent downtrend.',
+ },
+ {
+ symbol: 'HCLTECH',
+ company_name: 'HCL Technologies Ltd',
+ reason: 'SELL with 9.4% drop from recent highs. High risk rating with continued selling pressure.',
+ },
+ {
+ symbol: 'ADANIPORTS',
+ company_name: 'Adani Ports and SEZ Ltd',
+ reason: 'SELL with 12% monthly decline and consistently lower lows. High risk profile.',
+ },
+ ],
+ },
+];
+
+// Function to get recommendation for a specific date
+export function getRecommendationByDate(date: string): DailyRecommendation | undefined {
+ return sampleRecommendations.find(r => r.date === date);
+}
+
+// Function to get latest recommendation
+export function getLatestRecommendation(): DailyRecommendation | undefined {
+ return sampleRecommendations[0];
+}
+
+// Function to get all available dates
+export function getAvailableDates(): string[] {
+ return sampleRecommendations.map(r => r.date);
+}
+
+// Function to get stock history across all dates
+export function getStockHistory(symbol: string): { date: string; decision: Decision }[] {
+ return sampleRecommendations
+ .filter(r => r.analysis[symbol])
+ .map(r => ({
+ date: r.date,
+ decision: r.analysis[symbol].decision as Decision,
+ }))
+ .reverse();
+}
+
+// Get decision counts for charts
+export function getDecisionCounts(date: string): { buy: number; sell: number; hold: number } {
+ const rec = getRecommendationByDate(date);
+ if (!rec) return { buy: 0, sell: 0, hold: 0 };
+ return rec.summary;
+}
diff --git a/frontend/src/index.css b/frontend/src/index.css
new file mode 100644
index 00000000..c37e5a8c
--- /dev/null
+++ b/frontend/src/index.css
@@ -0,0 +1,94 @@
+@import "tailwindcss";
+
+@theme {
+ /* Custom colors */
+ --color-nifty-50: #f0f9ff;
+ --color-nifty-100: #e0f2fe;
+ --color-nifty-200: #bae6fd;
+ --color-nifty-300: #7dd3fc;
+ --color-nifty-400: #38bdf8;
+ --color-nifty-500: #0ea5e9;
+ --color-nifty-600: #0284c7;
+ --color-nifty-700: #0369a1;
+ --color-nifty-800: #075985;
+ --color-nifty-900: #0c4a6e;
+
+ --color-bull-light: #dcfce7;
+ --color-bull: #22c55e;
+ --color-bull-dark: #15803d;
+
+ --color-bear-light: #fee2e2;
+ --color-bear: #ef4444;
+ --color-bear-dark: #b91c1c;
+
+ --color-hold-light: #fef3c7;
+ --color-hold: #f59e0b;
+ --color-hold-dark: #b45309;
+
+ /* Custom fonts */
+ --font-sans: 'Inter', system-ui, sans-serif;
+ --font-display: 'Lexend', system-ui, sans-serif;
+}
+
+@layer base {
+ html {
+ scroll-behavior: smooth;
+ }
+
+ body {
+ @apply font-sans antialiased bg-gray-50 text-gray-900;
+ margin: 0;
+ }
+}
+
+@layer components {
+ .card {
+ @apply bg-white rounded-xl shadow-sm border border-gray-100 overflow-hidden;
+ }
+
+ .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;
+ }
+
+ .btn {
+ @apply inline-flex items-center justify-center px-4 py-2 rounded-lg font-medium transition-all duration-200;
+ }
+
+ .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;
+ }
+
+ .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;
+ }
+
+ .badge {
+ @apply inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium;
+ }
+
+ .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;
+ }
+
+ .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;
+ }
+
+ .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;
+ }
+
+ .gradient-text {
+ @apply bg-gradient-to-r from-nifty-600 to-nifty-800 bg-clip-text text-transparent;
+ }
+
+ .section-title {
+ @apply text-2xl font-display font-semibold text-gray-900;
+ }
+}
+
+@layer utilities {
+ .text-balance {
+ text-wrap: balance;
+ }
+}
diff --git a/frontend/src/main.tsx b/frontend/src/main.tsx
new file mode 100644
index 00000000..25e7a34e
--- /dev/null
+++ b/frontend/src/main.tsx
@@ -0,0 +1,13 @@
+import { StrictMode } from 'react';
+import { createRoot } from 'react-dom/client';
+import { BrowserRouter } from 'react-router-dom';
+import './index.css';
+import App from './App.tsx';
+
+createRoot(document.getElementById('root')!).render(
+
+
+
+
+ ,
+);
diff --git a/frontend/src/pages/Dashboard.tsx b/frontend/src/pages/Dashboard.tsx
new file mode 100644
index 00000000..6cf4abd2
--- /dev/null
+++ b/frontend/src/pages/Dashboard.tsx
@@ -0,0 +1,185 @@
+import { useState } from 'react';
+import { Link } from 'react-router-dom';
+import { Calendar, RefreshCw, Filter, ChevronRight, PieChart } from 'lucide-react';
+import SummaryStats from '../components/SummaryStats';
+import TopPicks, { StocksToAvoid } from '../components/TopPicks';
+import StockCard from '../components/StockCard';
+import { SummaryPieChart } from '../components/Charts';
+import { getLatestRecommendation } from '../data/recommendations';
+import type { Decision } from '../types';
+
+type FilterType = 'ALL' | Decision;
+
+export default function Dashboard() {
+ const recommendation = getLatestRecommendation();
+ const [filter, setFilter] = useState('ALL');
+
+ if (!recommendation) {
+ return (
+
+
+
+
Loading recommendations...
+
+
+ );
+ }
+
+ const stocks = Object.values(recommendation.analysis);
+ const filteredStocks = filter === 'ALL'
+ ? stocks
+ : stocks.filter(s => s.decision === filter);
+
+ const filterButtons: { label: string; value: FilterType; count: number }[] = [
+ { label: 'All', value: 'ALL', count: stocks.length },
+ { label: 'Buy', value: 'BUY', count: recommendation.summary.buy },
+ { label: 'Sell', value: 'SELL', count: recommendation.summary.sell },
+ { label: 'Hold', value: 'HOLD', count: recommendation.summary.hold },
+ ];
+
+ return (
+
+ {/* Hero Section */}
+
+
+ Nifty 50 AI Recommendations
+
+
+ AI-powered daily stock analysis for all Nifty 50 stocks. Get actionable buy, sell, and hold recommendations.
+
+
+
+ Last updated: {new Date(recommendation.date).toLocaleDateString('en-IN', {
+ weekday: 'long',
+ year: 'numeric',
+ month: 'long',
+ day: 'numeric',
+ })}
+
+
+
+ {/* Summary Stats */}
+
+
+ {/* Chart and Stats Section */}
+
+
+
+
+
Decision Distribution
+
+
+
+
+
+
Quick Analysis
+
+
+
+ Bullish Signals
+ {recommendation.summary.buy}
+
+
+ {((recommendation.summary.buy / recommendation.summary.total) * 100).toFixed(0)}% of stocks show buying opportunities
+
+
+
+
+ Neutral Position
+ {recommendation.summary.hold}
+
+
+ {((recommendation.summary.hold / recommendation.summary.total) * 100).toFixed(0)}% of stocks recommend holding
+
+
+
+
+ Bearish Signals
+ {recommendation.summary.sell}
+
+
+ {((recommendation.summary.sell / recommendation.summary.total) * 100).toFixed(0)}% of stocks suggest selling
+
+
+
+
+
+
+ {/* Top Picks and Avoid Section */}
+
+
+
+
+
+ {/* All Stocks Section */}
+
+
+
+
+
+
All Stocks
+
+
+ {filterButtons.map(({ label, value, count }) => (
+ setFilter(value)}
+ className={`px-3 py-1.5 text-sm font-medium rounded-lg transition-colors ${
+ filter === value
+ ? 'bg-nifty-600 text-white'
+ : 'bg-gray-100 text-gray-600 hover:bg-gray-200'
+ }`}
+ >
+ {label} ({count})
+
+ ))}
+
+
+
+
+
+ {filteredStocks.map((stock) => (
+
+ ))}
+
+
+ {filteredStocks.length === 0 && (
+
+
No stocks match the selected filter.
+
+ )}
+
+
+ {/* CTA Section */}
+
+
+
+
+ Track Historical Recommendations
+
+
+ View past recommendations and track how our AI predictions performed over time.
+
+
+
+ View History
+
+
+
+
+
+ );
+}
diff --git a/frontend/src/pages/History.tsx b/frontend/src/pages/History.tsx
new file mode 100644
index 00000000..137fc558
--- /dev/null
+++ b/frontend/src/pages/History.tsx
@@ -0,0 +1,132 @@
+import { useState } from 'react';
+import { Link } from 'react-router-dom';
+import { Calendar, TrendingUp, TrendingDown, Minus, ChevronRight, BarChart3 } from 'lucide-react';
+import { sampleRecommendations } from '../data/recommendations';
+import { DecisionBadge } from '../components/StockCard';
+
+export default function History() {
+ const [selectedDate, setSelectedDate] = useState(null);
+
+ const dates = sampleRecommendations.map(r => r.date);
+
+ const getRecommendation = (date: string) => {
+ return sampleRecommendations.find(r => r.date === date);
+ };
+
+ return (
+
+ {/* Header */}
+
+
+ Historical Recommendations
+
+
+ Browse past AI recommendations and track performance over time.
+
+
+
+ {/* Date Selector */}
+
+
+
+
Select Date
+
+
+ {dates.map((date) => {
+ const rec = getRecommendation(date);
+ return (
+
setSelectedDate(selectedDate === date ? null : date)}
+ className={`px-4 py-2 rounded-lg text-sm font-medium transition-all ${
+ selectedDate === date
+ ? 'bg-nifty-600 text-white'
+ : 'bg-gray-100 text-gray-700 hover:bg-gray-200'
+ }`}
+ >
+ {new Date(date).toLocaleDateString('en-IN', { month: 'short', day: 'numeric' })}
+
+ {rec?.summary.buy}B / {rec?.summary.sell}S / {rec?.summary.hold}H
+
+
+ );
+ })}
+
+
+
+ {/* Selected Date Details */}
+ {selectedDate && (
+
+
+
+
+ {new Date(selectedDate).toLocaleDateString('en-IN', {
+ weekday: 'long',
+ year: 'numeric',
+ month: 'long',
+ day: 'numeric',
+ })}
+
+
+
+
+ {getRecommendation(selectedDate)?.summary.buy} Buy
+
+
+
+ {getRecommendation(selectedDate)?.summary.sell} Sell
+
+
+
+ {getRecommendation(selectedDate)?.summary.hold} Hold
+
+
+
+
+
+
+ {Object.values(getRecommendation(selectedDate)?.analysis || {}).map((stock) => (
+
+
+
+ {stock.symbol}
+
+
+
{stock.company_name}
+
+
+
+ ))}
+
+
+ )}
+
+ {/* Summary Cards */}
+
+
+
+
{dates.length}
+
Days of Analysis
+
+
+
+
+ {sampleRecommendations.reduce((acc, r) => acc + r.summary.buy, 0)}
+
+
Total Buy Signals
+
+
+
+
+ {sampleRecommendations.reduce((acc, r) => acc + r.summary.sell, 0)}
+
+
Total Sell Signals
+
+
+
+ );
+}
diff --git a/frontend/src/pages/StockDetail.tsx b/frontend/src/pages/StockDetail.tsx
new file mode 100644
index 00000000..1e72eb22
--- /dev/null
+++ b/frontend/src/pages/StockDetail.tsx
@@ -0,0 +1,231 @@
+import { useParams, Link } from 'react-router-dom';
+import { ArrowLeft, Building2, TrendingUp, TrendingDown, Minus, AlertTriangle, Info, Calendar, Activity } from 'lucide-react';
+import { NIFTY_50_STOCKS } from '../types';
+import { sampleRecommendations, getStockHistory } from '../data/recommendations';
+import { DecisionBadge, ConfidenceBadge, RiskBadge } from '../components/StockCard';
+
+export default function StockDetail() {
+ const { symbol } = useParams<{ symbol: string }>();
+
+ const stock = NIFTY_50_STOCKS.find(s => s.symbol === symbol);
+ const latestRecommendation = sampleRecommendations[0];
+ const analysis = latestRecommendation?.analysis[symbol || ''];
+ const history = symbol ? getStockHistory(symbol) : [];
+
+ if (!stock) {
+ return (
+
+
+
+
Stock Not Found
+
The stock "{symbol}" was not found in Nifty 50.
+
+ View All Stocks
+
+
+
+ );
+ }
+
+ const decisionIcon = {
+ BUY: TrendingUp,
+ SELL: TrendingDown,
+ HOLD: Minus,
+ };
+
+ const decisionColor = {
+ BUY: 'from-green-500 to-green-600',
+ SELL: 'from-red-500 to-red-600',
+ HOLD: 'from-amber-500 to-amber-600',
+ };
+
+ const DecisionIcon = analysis?.decision ? decisionIcon[analysis.decision] : Activity;
+ const bgGradient = analysis?.decision ? decisionColor[analysis.decision] : 'from-gray-500 to-gray-600';
+
+ return (
+
+ {/* Back Button */}
+
+
+
+ Back to All Stocks
+
+
+
+ {/* Stock Header */}
+
+
+
+
+
+
{stock.symbol}
+ {analysis?.decision && (
+
+
+ {analysis.decision}
+
+ )}
+
+
{stock.company_name}
+
+
+ {stock.sector || 'N/A'}
+
+
+
+
Latest Analysis
+
+
+ {latestRecommendation?.date ? new Date(latestRecommendation.date).toLocaleDateString('en-IN', {
+ month: 'short',
+ day: 'numeric',
+ year: 'numeric',
+ }) : 'N/A'}
+
+
+
+
+
+ {/* Analysis Details */}
+
+ {analysis ? (
+
+
+
Decision
+
+
+
+
Confidence
+
+
+
+
Risk Level
+
+
+
+ ) : (
+
+
+ No analysis available for this stock yet.
+
+ )}
+
+
+
+ {/* Quick Stats Grid */}
+
+
+
{history.length}
+
Total Analyses
+
+
+
+ {history.filter(h => h.decision === 'BUY').length}
+
+
Buy Signals
+
+
+
+ {history.filter(h => h.decision === 'HOLD').length}
+
+
Hold Signals
+
+
+
+ {history.filter(h => h.decision === 'SELL').length}
+
+
Sell Signals
+
+
+
+ {/* Analysis History */}
+
+
+
Recommendation History
+
+
+ {history.length > 0 ? (
+
+ {history.map((entry, idx) => (
+
+
+
+ {new Date(entry.date).toLocaleDateString('en-IN', {
+ weekday: 'short',
+ month: 'short',
+ day: 'numeric',
+ year: 'numeric',
+ })}
+
+
+
+
+ ))}
+
+ ) : (
+
+
+
No History Yet
+
Recommendation history will appear here as we analyze this stock daily.
+
+ )}
+
+
+ {/* Top Pick / Avoid Status */}
+ {latestRecommendation && (
+ <>
+ {latestRecommendation.top_picks.some(p => p.symbol === symbol) && (
+
+
+
+
+
+
+
+
Top Pick
+
+ {latestRecommendation.top_picks.find(p => p.symbol === symbol)?.reason}
+
+
+
+
+
+ )}
+
+ {latestRecommendation.stocks_to_avoid.some(s => s.symbol === symbol) && (
+
+
+
+
+
+
Stock to Avoid
+
+ {latestRecommendation.stocks_to_avoid.find(s => s.symbol === symbol)?.reason}
+
+
+
+
+
+ )}
+ >
+ )}
+
+ {/* Disclaimer */}
+
+
+
+
+ Disclaimer: This AI-generated recommendation is for educational purposes only.
+ It should not be considered as financial advice. Always do your own research and consult with
+ a qualified financial advisor before making investment decisions.
+
+
+
+
+ );
+}
diff --git a/frontend/src/pages/Stocks.tsx b/frontend/src/pages/Stocks.tsx
new file mode 100644
index 00000000..e96b5384
--- /dev/null
+++ b/frontend/src/pages/Stocks.tsx
@@ -0,0 +1,131 @@
+import { useState, useMemo } from 'react';
+import { Link } from 'react-router-dom';
+import { Search, Filter, ChevronRight, Building2 } from 'lucide-react';
+import { NIFTY_50_STOCKS } from '../types';
+import { getLatestRecommendation } from '../data/recommendations';
+import { DecisionBadge, ConfidenceBadge } from '../components/StockCard';
+
+export default function Stocks() {
+ const [search, setSearch] = useState('');
+ const [sectorFilter, setSectorFilter] = useState('ALL');
+
+ const recommendation = getLatestRecommendation();
+
+ const sectors = useMemo(() => {
+ const sectorSet = new Set(NIFTY_50_STOCKS.map(s => s.sector).filter(Boolean));
+ return ['ALL', ...Array.from(sectorSet).sort()];
+ }, []);
+
+ const filteredStocks = useMemo(() => {
+ return NIFTY_50_STOCKS.filter(stock => {
+ const matchesSearch =
+ stock.symbol.toLowerCase().includes(search.toLowerCase()) ||
+ stock.company_name.toLowerCase().includes(search.toLowerCase());
+
+ const matchesSector = sectorFilter === 'ALL' || stock.sector === sectorFilter;
+
+ return matchesSearch && matchesSector;
+ });
+ }, [search, sectorFilter]);
+
+ const getStockAnalysis = (symbol: string) => {
+ return recommendation?.analysis[symbol];
+ };
+
+ return (
+
+ {/* Header */}
+
+
+ All Nifty 50 Stocks
+
+
+ Browse all 50 stocks in the Nifty index with their latest AI recommendations.
+
+
+
+ {/* Search and Filter */}
+
+
+ {/* Search */}
+
+
+ setSearch(e.target.value)}
+ className="w-full pl-10 pr-4 py-2.5 rounded-lg border border-gray-200 focus:border-nifty-500 focus:ring-2 focus:ring-nifty-500/20 outline-none transition-all"
+ />
+
+
+ {/* Sector Filter */}
+
+
+ setSectorFilter(e.target.value)}
+ className="px-4 py-2.5 rounded-lg border border-gray-200 focus:border-nifty-500 focus:ring-2 focus:ring-nifty-500/20 outline-none bg-white"
+ >
+ {sectors.map((sector) => (
+
+ {sector === 'ALL' ? 'All Sectors' : sector}
+
+ ))}
+
+
+
+
+
+ Showing {filteredStocks.length} of {NIFTY_50_STOCKS.length} stocks
+
+
+
+ {/* Stocks Grid */}
+
+ {filteredStocks.map((stock) => {
+ const analysis = getStockAnalysis(stock.symbol);
+ return (
+
+
+
+
{stock.symbol}
+
{stock.company_name}
+
+
+
+
+
+
+ {stock.sector}
+
+
+ {analysis && (
+
+ )}
+
+ );
+ })}
+
+
+ {filteredStocks.length === 0 && (
+
+
+
No stocks found
+
Try adjusting your search or filter criteria.
+
+ )}
+
+ );
+}
diff --git a/frontend/src/services/api.ts b/frontend/src/services/api.ts
new file mode 100644
index 00000000..1bb884cc
--- /dev/null
+++ b/frontend/src/services/api.ts
@@ -0,0 +1,139 @@
+/**
+ * API service for fetching stock recommendations from the backend.
+ */
+
+const API_BASE_URL = import.meta.env.VITE_API_URL || 'http://localhost:8000';
+
+export interface StockAnalysis {
+ symbol: string;
+ company_name: string;
+ decision: 'BUY' | 'SELL' | 'HOLD' | null;
+ confidence?: 'HIGH' | 'MEDIUM' | 'LOW';
+ risk?: 'HIGH' | 'MEDIUM' | 'LOW';
+ raw_analysis?: string;
+}
+
+export interface TopPick {
+ rank: number;
+ symbol: string;
+ company_name: string;
+ decision: string;
+ reason: string;
+ risk_level: string;
+}
+
+export interface StockToAvoid {
+ symbol: string;
+ company_name: string;
+ reason: string;
+}
+
+export interface Summary {
+ total: number;
+ buy: number;
+ sell: number;
+ hold: number;
+}
+
+export interface DailyRecommendation {
+ date: string;
+ analysis: Record;
+ summary: Summary;
+ top_picks: TopPick[];
+ stocks_to_avoid: StockToAvoid[];
+}
+
+export interface StockHistory {
+ date: string;
+ decision: string;
+ confidence?: string;
+ risk?: string;
+}
+
+class ApiService {
+ private baseUrl: string;
+
+ constructor() {
+ this.baseUrl = API_BASE_URL;
+ }
+
+ private async fetch(endpoint: string, options?: RequestInit): Promise {
+ const url = `${this.baseUrl}${endpoint}`;
+ const response = await fetch(url, {
+ ...options,
+ headers: {
+ 'Content-Type': 'application/json',
+ ...options?.headers,
+ },
+ });
+
+ if (!response.ok) {
+ throw new Error(`API Error: ${response.status} ${response.statusText}`);
+ }
+
+ return response.json();
+ }
+
+ /**
+ * Get all daily recommendations
+ */
+ async getAllRecommendations(): Promise<{ recommendations: DailyRecommendation[]; count: number }> {
+ return this.fetch('/recommendations');
+ }
+
+ /**
+ * Get the latest recommendation
+ */
+ async getLatestRecommendation(): Promise {
+ return this.fetch('/recommendations/latest');
+ }
+
+ /**
+ * Get recommendation for a specific date
+ */
+ async getRecommendationByDate(date: string): Promise {
+ return this.fetch(`/recommendations/${date}`);
+ }
+
+ /**
+ * Get historical recommendations for a stock
+ */
+ async getStockHistory(symbol: string): Promise<{ symbol: string; history: StockHistory[]; count: number }> {
+ return this.fetch(`/stocks/${symbol}/history`);
+ }
+
+ /**
+ * Get all available dates
+ */
+ async getAvailableDates(): Promise<{ dates: string[]; count: number }> {
+ return this.fetch('/dates');
+ }
+
+ /**
+ * Health check
+ */
+ async healthCheck(): Promise<{ status: string; database: string }> {
+ return this.fetch('/health');
+ }
+
+ /**
+ * Save a new recommendation (used by the analyzer)
+ */
+ async saveRecommendation(recommendation: {
+ date: string;
+ analysis: Record;
+ summary: Summary;
+ top_picks: TopPick[];
+ stocks_to_avoid: StockToAvoid[];
+ }): Promise<{ message: string }> {
+ return this.fetch('/recommendations', {
+ method: 'POST',
+ body: JSON.stringify(recommendation),
+ });
+ }
+}
+
+export const api = new ApiService();
+
+// Export a hook-friendly version for React Query or SWR
+export default api;
diff --git a/frontend/src/types/index.ts b/frontend/src/types/index.ts
new file mode 100644
index 00000000..db455e17
--- /dev/null
+++ b/frontend/src/types/index.ts
@@ -0,0 +1,130 @@
+export type Decision = 'BUY' | 'SELL' | 'HOLD';
+export type Confidence = 'HIGH' | 'MEDIUM' | 'LOW';
+export type Risk = 'HIGH' | 'MEDIUM' | 'LOW';
+
+export interface StockAnalysis {
+ symbol: string;
+ company_name: string;
+ decision: Decision | null;
+ confidence?: Confidence;
+ risk?: Risk;
+ raw_analysis?: string;
+ error?: string | null;
+}
+
+export interface RankingResult {
+ ranking: string;
+ stocks_analyzed: number;
+ timestamp: string;
+ error?: string;
+}
+
+export interface TopPick {
+ rank: number;
+ symbol: string;
+ company_name: string;
+ decision: string;
+ reason: string;
+ risk_level: Risk;
+}
+
+export interface StockToAvoid {
+ symbol: string;
+ company_name: string;
+ reason: string;
+}
+
+export interface DailyRecommendation {
+ date: string;
+ analysis: Record;
+ ranking: RankingResult;
+ summary: {
+ total: number;
+ buy: number;
+ sell: number;
+ hold: number;
+ };
+ top_picks: TopPick[];
+ stocks_to_avoid: StockToAvoid[];
+}
+
+export interface HistoricalEntry {
+ date: string;
+ symbol: string;
+ company_name: string;
+ decision: Decision;
+ confidence?: Confidence;
+ risk?: Risk;
+}
+
+export interface StockHistory {
+ symbol: string;
+ company_name: string;
+ history: HistoricalEntry[];
+ stats: {
+ total_recommendations: number;
+ buy_count: number;
+ sell_count: number;
+ hold_count: number;
+ accuracy?: number;
+ };
+}
+
+export interface NiftyStock {
+ symbol: string;
+ company_name: string;
+ sector?: string;
+}
+
+export const NIFTY_50_STOCKS: NiftyStock[] = [
+ { symbol: 'RELIANCE', company_name: 'Reliance Industries Ltd', sector: 'Energy' },
+ { symbol: 'TCS', company_name: 'Tata Consultancy Services Ltd', sector: 'IT' },
+ { symbol: 'HDFCBANK', company_name: 'HDFC Bank Ltd', sector: 'Banking' },
+ { symbol: 'INFY', company_name: 'Infosys Ltd', sector: 'IT' },
+ { symbol: 'ICICIBANK', company_name: 'ICICI Bank Ltd', sector: 'Banking' },
+ { symbol: 'HINDUNILVR', company_name: 'Hindustan Unilever Ltd', sector: 'FMCG' },
+ { symbol: 'ITC', company_name: 'ITC Ltd', sector: 'FMCG' },
+ { symbol: 'SBIN', company_name: 'State Bank of India', sector: 'Banking' },
+ { symbol: 'BHARTIARTL', company_name: 'Bharti Airtel Ltd', sector: 'Telecom' },
+ { symbol: 'KOTAKBANK', company_name: 'Kotak Mahindra Bank Ltd', sector: 'Banking' },
+ { symbol: 'LT', company_name: 'Larsen & Toubro Ltd', sector: 'Infrastructure' },
+ { symbol: 'AXISBANK', company_name: 'Axis Bank Ltd', sector: 'Banking' },
+ { symbol: 'ASIANPAINT', company_name: 'Asian Paints Ltd', sector: 'Consumer' },
+ { symbol: 'MARUTI', company_name: 'Maruti Suzuki India Ltd', sector: 'Auto' },
+ { symbol: 'HCLTECH', company_name: 'HCL Technologies Ltd', sector: 'IT' },
+ { symbol: 'SUNPHARMA', company_name: 'Sun Pharmaceutical Industries Ltd', sector: 'Pharma' },
+ { symbol: 'TITAN', company_name: 'Titan Company Ltd', sector: 'Consumer' },
+ { symbol: 'BAJFINANCE', company_name: 'Bajaj Finance Ltd', sector: 'Finance' },
+ { symbol: 'WIPRO', company_name: 'Wipro Ltd', sector: 'IT' },
+ { symbol: 'ULTRACEMCO', company_name: 'UltraTech Cement Ltd', sector: 'Cement' },
+ { symbol: 'NESTLEIND', company_name: 'Nestle India Ltd', sector: 'FMCG' },
+ { symbol: 'NTPC', company_name: 'NTPC Ltd', sector: 'Power' },
+ { symbol: 'POWERGRID', company_name: 'Power Grid Corporation of India Ltd', sector: 'Power' },
+ { symbol: 'M&M', company_name: 'Mahindra & Mahindra Ltd', sector: 'Auto' },
+ { symbol: 'TATAMOTORS', company_name: 'Tata Motors Ltd', sector: 'Auto' },
+ { symbol: 'ONGC', company_name: 'Oil & Natural Gas Corporation Ltd', sector: 'Energy' },
+ { symbol: 'JSWSTEEL', company_name: 'JSW Steel Ltd', sector: 'Metals' },
+ { symbol: 'TATASTEEL', company_name: 'Tata Steel Ltd', sector: 'Metals' },
+ { symbol: 'ADANIENT', company_name: 'Adani Enterprises Ltd', sector: 'Conglomerate' },
+ { symbol: 'ADANIPORTS', company_name: 'Adani Ports and SEZ Ltd', sector: 'Infrastructure' },
+ { symbol: 'COALINDIA', company_name: 'Coal India Ltd', sector: 'Mining' },
+ { symbol: 'BAJAJFINSV', company_name: 'Bajaj Finserv Ltd', sector: 'Finance' },
+ { symbol: 'TECHM', company_name: 'Tech Mahindra Ltd', sector: 'IT' },
+ { symbol: 'HDFCLIFE', company_name: 'HDFC Life Insurance Company Ltd', sector: 'Insurance' },
+ { symbol: 'SBILIFE', company_name: 'SBI Life Insurance Company Ltd', sector: 'Insurance' },
+ { symbol: 'GRASIM', company_name: 'Grasim Industries Ltd', sector: 'Cement' },
+ { symbol: 'DIVISLAB', company_name: "Divi's Laboratories Ltd", sector: 'Pharma' },
+ { symbol: 'DRREDDY', company_name: "Dr. Reddy's Laboratories Ltd", sector: 'Pharma' },
+ { symbol: 'CIPLA', company_name: 'Cipla Ltd', sector: 'Pharma' },
+ { symbol: 'BRITANNIA', company_name: 'Britannia Industries Ltd', sector: 'FMCG' },
+ { symbol: 'EICHERMOT', company_name: 'Eicher Motors Ltd', sector: 'Auto' },
+ { symbol: 'APOLLOHOSP', company_name: 'Apollo Hospitals Enterprise Ltd', sector: 'Healthcare' },
+ { symbol: 'INDUSINDBK', company_name: 'IndusInd Bank Ltd', sector: 'Banking' },
+ { symbol: 'HEROMOTOCO', company_name: 'Hero MotoCorp Ltd', sector: 'Auto' },
+ { symbol: 'TATACONSUM', company_name: 'Tata Consumer Products Ltd', sector: 'FMCG' },
+ { symbol: 'BPCL', company_name: 'Bharat Petroleum Corporation Ltd', sector: 'Energy' },
+ { symbol: 'UPL', company_name: 'UPL Ltd', sector: 'Chemicals' },
+ { symbol: 'HINDALCO', company_name: 'Hindalco Industries Ltd', sector: 'Metals' },
+ { symbol: 'BAJAJ-AUTO', company_name: 'Bajaj Auto Ltd', sector: 'Auto' },
+ { symbol: 'LTIM', company_name: 'LTIMindtree Ltd', sector: 'IT' },
+];
diff --git a/frontend/tailwind.config.js b/frontend/tailwind.config.js
new file mode 100644
index 00000000..c1e41357
--- /dev/null
+++ b/frontend/tailwind.config.js
@@ -0,0 +1,45 @@
+/** @type {import('tailwindcss').Config} */
+export default {
+ content: [
+ "./index.html",
+ "./src/**/*.{js,ts,jsx,tsx}",
+ ],
+ theme: {
+ extend: {
+ colors: {
+ 'nifty': {
+ 50: '#f0f9ff',
+ 100: '#e0f2fe',
+ 200: '#bae6fd',
+ 300: '#7dd3fc',
+ 400: '#38bdf8',
+ 500: '#0ea5e9',
+ 600: '#0284c7',
+ 700: '#0369a1',
+ 800: '#075985',
+ 900: '#0c4a6e',
+ },
+ 'bull': {
+ light: '#dcfce7',
+ DEFAULT: '#22c55e',
+ dark: '#15803d',
+ },
+ 'bear': {
+ light: '#fee2e2',
+ DEFAULT: '#ef4444',
+ dark: '#b91c1c',
+ },
+ 'hold': {
+ light: '#fef3c7',
+ DEFAULT: '#f59e0b',
+ dark: '#b45309',
+ }
+ },
+ fontFamily: {
+ sans: ['Inter', 'system-ui', 'sans-serif'],
+ display: ['Lexend', 'system-ui', 'sans-serif'],
+ },
+ },
+ },
+ plugins: [],
+}
diff --git a/frontend/tsconfig.app.json b/frontend/tsconfig.app.json
new file mode 100644
index 00000000..a9b5a59c
--- /dev/null
+++ b/frontend/tsconfig.app.json
@@ -0,0 +1,28 @@
+{
+ "compilerOptions": {
+ "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
+ "target": "ES2022",
+ "useDefineForClassFields": true,
+ "lib": ["ES2022", "DOM", "DOM.Iterable"],
+ "module": "ESNext",
+ "types": ["vite/client"],
+ "skipLibCheck": true,
+
+ /* Bundler mode */
+ "moduleResolution": "bundler",
+ "allowImportingTsExtensions": true,
+ "verbatimModuleSyntax": true,
+ "moduleDetection": "force",
+ "noEmit": true,
+ "jsx": "react-jsx",
+
+ /* Linting */
+ "strict": true,
+ "noUnusedLocals": true,
+ "noUnusedParameters": true,
+ "erasableSyntaxOnly": true,
+ "noFallthroughCasesInSwitch": true,
+ "noUncheckedSideEffectImports": true
+ },
+ "include": ["src"]
+}
diff --git a/frontend/tsconfig.json b/frontend/tsconfig.json
new file mode 100644
index 00000000..1ffef600
--- /dev/null
+++ b/frontend/tsconfig.json
@@ -0,0 +1,7 @@
+{
+ "files": [],
+ "references": [
+ { "path": "./tsconfig.app.json" },
+ { "path": "./tsconfig.node.json" }
+ ]
+}
diff --git a/frontend/tsconfig.node.json b/frontend/tsconfig.node.json
new file mode 100644
index 00000000..8a67f62f
--- /dev/null
+++ b/frontend/tsconfig.node.json
@@ -0,0 +1,26 @@
+{
+ "compilerOptions": {
+ "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
+ "target": "ES2023",
+ "lib": ["ES2023"],
+ "module": "ESNext",
+ "types": ["node"],
+ "skipLibCheck": true,
+
+ /* Bundler mode */
+ "moduleResolution": "bundler",
+ "allowImportingTsExtensions": true,
+ "verbatimModuleSyntax": true,
+ "moduleDetection": "force",
+ "noEmit": true,
+
+ /* Linting */
+ "strict": true,
+ "noUnusedLocals": true,
+ "noUnusedParameters": true,
+ "erasableSyntaxOnly": true,
+ "noFallthroughCasesInSwitch": true,
+ "noUncheckedSideEffectImports": true
+ },
+ "include": ["vite.config.ts"]
+}
diff --git a/frontend/vite.config.ts b/frontend/vite.config.ts
new file mode 100644
index 00000000..8c9413b9
--- /dev/null
+++ b/frontend/vite.config.ts
@@ -0,0 +1,16 @@
+import { defineConfig } from 'vite'
+import react from '@vitejs/plugin-react'
+import tailwindcss from '@tailwindcss/vite'
+
+// https://vite.dev/config/
+export default defineConfig({
+ plugins: [react(), tailwindcss()],
+ server: {
+ host: '0.0.0.0',
+ port: 5173,
+ cors: true,
+ hmr: {
+ host: '192.168.3.200',
+ },
+ },
+})
diff --git a/nifty50_recommend.py b/nifty50_recommend.py
new file mode 100644
index 00000000..d466bddb
--- /dev/null
+++ b/nifty50_recommend.py
@@ -0,0 +1,184 @@
+#!/usr/bin/env python3
+"""
+Nifty 50 Stock Recommendation CLI.
+
+This script runs the Nifty 50 recommendation system to predict all Nifty 50 stocks
+and select the ones with highest short-term growth potential using Claude Opus 4.5.
+
+Usage:
+ # Full run (all 50 stocks)
+ python nifty50_recommend.py --date 2025-01-30
+
+ # Test with specific stocks
+ python nifty50_recommend.py --stocks TCS,INFY,RELIANCE --date 2025-01-30
+
+ # Quiet mode (less output)
+ python nifty50_recommend.py --date 2025-01-30 --quiet
+"""
+
+import argparse
+import sys
+from datetime import datetime
+
+
+def parse_args():
+ """Parse command line arguments."""
+ parser = argparse.ArgumentParser(
+ description="Nifty 50 Stock Recommendation System using Claude Opus 4.5",
+ formatter_class=argparse.RawDescriptionHelpFormatter,
+ epilog="""
+Examples:
+ # Analyze all 50 Nifty stocks
+ python nifty50_recommend.py --date 2025-01-30
+
+ # Analyze specific stocks only
+ python nifty50_recommend.py --stocks TCS,INFY,RELIANCE --date 2025-01-30
+
+ # Save results to custom directory
+ python nifty50_recommend.py --date 2025-01-30 --output ./my_results
+
+ # Quick test with 3 stocks
+ python nifty50_recommend.py --stocks TCS,INFY,RELIANCE --date 2025-01-30
+ """
+ )
+
+ parser.add_argument(
+ "--date",
+ "-d",
+ type=str,
+ default=datetime.now().strftime("%Y-%m-%d"),
+ help="Analysis date in YYYY-MM-DD format (default: today)"
+ )
+
+ parser.add_argument(
+ "--stocks",
+ "-s",
+ type=str,
+ default=None,
+ help="Comma-separated list of stock symbols to analyze (default: all 50)"
+ )
+
+ parser.add_argument(
+ "--output",
+ "-o",
+ type=str,
+ default=None,
+ help="Directory to save results (default: ./results/nifty50_recommendations)"
+ )
+
+ parser.add_argument(
+ "--quiet",
+ "-q",
+ action="store_true",
+ help="Suppress progress output"
+ )
+
+ parser.add_argument(
+ "--no-save",
+ action="store_true",
+ help="Don't save results to disk"
+ )
+
+ parser.add_argument(
+ "--test-credentials",
+ action="store_true",
+ help="Only test Claude credentials and exit"
+ )
+
+ return parser.parse_args()
+
+
+def test_credentials():
+ """Test if Claude credentials are valid."""
+ from tradingagents.nifty50_recommender import get_claude_credentials
+
+ try:
+ token = get_claude_credentials()
+ print(f"✓ Claude credentials found")
+ print(f" Token prefix: {token[:20]}...")
+ return True
+ except FileNotFoundError as e:
+ print(f"✗ Credentials file not found: {e}")
+ return False
+ except KeyError as e:
+ print(f"✗ Invalid credentials format: {e}")
+ return False
+ except Exception as e:
+ print(f"✗ Error reading credentials: {e}")
+ return False
+
+
+def main():
+ """Main entry point."""
+ args = parse_args()
+
+ # Test credentials mode
+ if args.test_credentials:
+ success = test_credentials()
+ sys.exit(0 if success else 1)
+
+ # Validate date format
+ try:
+ datetime.strptime(args.date, "%Y-%m-%d")
+ except ValueError:
+ print(f"Error: Invalid date format '{args.date}'. Use YYYY-MM-DD")
+ sys.exit(1)
+
+ # Parse stock subset
+ stock_subset = None
+ if args.stocks:
+ stock_subset = [s.strip().upper() for s in args.stocks.split(",")]
+ print(f"Analyzing subset: {', '.join(stock_subset)}")
+
+ # Import the simplified recommender (works with Claude Max subscription)
+ try:
+ from tradingagents.nifty50_simple_recommender import run_recommendation
+ except ImportError as e:
+ print(f"Error importing recommender module: {e}")
+ print("Make sure you're running from the TradingAgents directory")
+ sys.exit(1)
+
+ # Run the recommendation
+ try:
+ predictions, ranking = run_recommendation(
+ trade_date=args.date,
+ stock_subset=stock_subset,
+ save_results=not args.no_save,
+ results_dir=args.output,
+ verbose=not args.quiet
+ )
+
+ # Print summary
+ if not args.quiet:
+ print("\n" + "="*60)
+ print("SUMMARY")
+ print("="*60)
+
+ successful = sum(1 for p in predictions.values() if not p.get("error"))
+ print(f"Stocks analyzed: {successful}/{len(predictions)}")
+
+ # Count decisions
+ decisions = {}
+ for p in predictions.values():
+ if not p.get("error"):
+ decision = str(p.get("decision", "UNKNOWN"))
+ decisions[decision] = decisions.get(decision, 0) + 1
+
+ print("\nDecision breakdown:")
+ for decision, count in sorted(decisions.items()):
+ print(f" {decision}: {count}")
+
+ except FileNotFoundError as e:
+ print(f"\nError: {e}")
+ print("\nTo authenticate with Claude, run:")
+ print(" claude auth login")
+ sys.exit(1)
+ except Exception as e:
+ print(f"\nError running recommendation: {e}")
+ import traceback
+ traceback.print_exc()
+ sys.exit(1)
+
+
+if __name__ == "__main__":
+ main()
diff --git a/package.json b/package.json
new file mode 100644
index 00000000..dc0ba8ba
--- /dev/null
+++ b/package.json
@@ -0,0 +1,23 @@
+{
+ "name": "tradingagents",
+ "version": "1.0.0",
+ "description": "
",
+ "main": "index.js",
+ "scripts": {
+ "test": "echo \"Error: no test specified\" && exit 1"
+ },
+ "repository": {
+ "type": "git",
+ "url": "git+https://github.com/TauricResearch/TradingAgents.git"
+ },
+ "keywords": [],
+ "author": "",
+ "license": "ISC",
+ "bugs": {
+ "url": "https://github.com/TauricResearch/TradingAgents/issues"
+ },
+ "homepage": "https://github.com/TauricResearch/TradingAgents#readme",
+ "dependencies": {
+ "playwright": "^1.58.1"
+ }
+}
diff --git a/requirements.txt b/requirements.txt
index a6154cd2..b29d878a 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -24,3 +24,5 @@ rich
questionary
langchain_anthropic
langchain-google-genai
+jugaad-data
+sentence-transformers
diff --git a/tradingagents/agents/utils/memory.py b/tradingagents/agents/utils/memory.py
index 69b8ab8c..04bf2c52 100644
--- a/tradingagents/agents/utils/memory.py
+++ b/tradingagents/agents/utils/memory.py
@@ -1,25 +1,31 @@
import chromadb
from chromadb.config import Settings
-from openai import OpenAI
+from chromadb.utils import embedding_functions
class FinancialSituationMemory:
+ """Memory system for storing and retrieving financial situations using local embeddings."""
+
def __init__(self, name, config):
- if config["backend_url"] == "http://localhost:11434/v1":
- self.embedding = "nomic-embed-text"
- else:
- self.embedding = "text-embedding-3-small"
- self.client = OpenAI(base_url=config["backend_url"])
+ """
+ Initialize the memory system with local embeddings.
+
+ Args:
+ name: Name for the ChromaDB collection
+ config: Configuration dictionary (kept for compatibility)
+ """
+ # Use ChromaDB's default embedding function (uses all-MiniLM-L6-v2 internally)
+ self.embedding_fn = embedding_functions.DefaultEmbeddingFunction()
self.chroma_client = chromadb.Client(Settings(allow_reset=True))
- self.situation_collection = self.chroma_client.create_collection(name=name)
+ self.situation_collection = self.chroma_client.create_collection(
+ name=name,
+ embedding_function=self.embedding_fn
+ )
def get_embedding(self, text):
- """Get OpenAI embedding for a text"""
-
- response = self.client.embeddings.create(
- model=self.embedding, input=text
- )
- return response.data[0].embedding
+ """Get embedding for a text using the embedding function."""
+ embeddings = self.embedding_fn([text])
+ return embeddings[0]
def add_situations(self, situations_and_advice):
"""Add financial situations and their corresponding advice. Parameter is a list of tuples (situation, rec)"""
@@ -27,7 +33,6 @@ class FinancialSituationMemory:
situations = []
advice = []
ids = []
- embeddings = []
offset = self.situation_collection.count()
@@ -35,41 +40,39 @@ class FinancialSituationMemory:
situations.append(situation)
advice.append(recommendation)
ids.append(str(offset + i))
- embeddings.append(self.get_embedding(situation))
+ # Let ChromaDB handle embeddings automatically
self.situation_collection.add(
documents=situations,
metadatas=[{"recommendation": rec} for rec in advice],
- embeddings=embeddings,
ids=ids,
)
def get_memories(self, current_situation, n_matches=1):
- """Find matching recommendations using OpenAI embeddings"""
- query_embedding = self.get_embedding(current_situation)
-
+ """Find matching recommendations using embeddings"""
results = self.situation_collection.query(
- query_embeddings=[query_embedding],
+ query_texts=[current_situation],
n_results=n_matches,
include=["metadatas", "documents", "distances"],
)
matched_results = []
- for i in range(len(results["documents"][0])):
- matched_results.append(
- {
- "matched_situation": results["documents"][0][i],
- "recommendation": results["metadatas"][0][i]["recommendation"],
- "similarity_score": 1 - results["distances"][0][i],
- }
- )
+ if results["documents"] and results["documents"][0]:
+ for i in range(len(results["documents"][0])):
+ matched_results.append(
+ {
+ "matched_situation": results["documents"][0][i],
+ "recommendation": results["metadatas"][0][i]["recommendation"],
+ "similarity_score": 1 - results["distances"][0][i],
+ }
+ )
return matched_results
if __name__ == "__main__":
# Example usage
- matcher = FinancialSituationMemory()
+ matcher = FinancialSituationMemory("test_memory", {})
# Example data
example_data = [
@@ -96,7 +99,7 @@ if __name__ == "__main__":
# Example query
current_situation = """
- Market showing increased volatility in tech sector, with institutional investors
+ Market showing increased volatility in tech sector, with institutional investors
reducing positions and rising interest rates affecting growth stock valuations
"""
diff --git a/tradingagents/agents/utils/news_data_tools.py b/tradingagents/agents/utils/news_data_tools.py
index 0df9d047..8e9f2fb2 100644
--- a/tradingagents/agents/utils/news_data_tools.py
+++ b/tradingagents/agents/utils/news_data_tools.py
@@ -1,6 +1,7 @@
from langchain_core.tools import tool
from typing import Annotated
from tradingagents.dataflows.interface import route_to_vendor
+from tradingagents.dataflows.markets import is_nifty_50_stock
@tool
def get_news(
@@ -52,6 +53,12 @@ def get_insider_sentiment(
Returns:
str: A report of insider sentiment data
"""
+ # Check if this is an NSE stock - insider sentiment from SEC sources is not available
+ if is_nifty_50_stock(ticker):
+ return (f"Note: SEC-style insider sentiment data is not available for Indian NSE stocks like {ticker}. "
+ f"For Indian stocks, insider trading disclosures are regulated by SEBI (Securities and Exchange Board of India) "
+ f"and can be found on NSE/BSE websites or through the company's regulatory filings.")
+
return route_to_vendor("get_insider_sentiment", ticker, curr_date)
@tool
diff --git a/tradingagents/claude_max_llm.py b/tradingagents/claude_max_llm.py
new file mode 100644
index 00000000..a3e718ca
--- /dev/null
+++ b/tradingagents/claude_max_llm.py
@@ -0,0 +1,164 @@
+"""
+Claude Max LLM Wrapper.
+
+This module provides a LangChain-compatible LLM that uses the Claude CLI
+with Max subscription authentication instead of API keys.
+"""
+
+import os
+import subprocess
+import json
+from typing import Any, Dict, List, Optional, Iterator
+
+from langchain_core.language_models.chat_models import BaseChatModel
+from langchain_core.messages import (
+ AIMessage,
+ BaseMessage,
+ HumanMessage,
+ SystemMessage,
+)
+from langchain_core.outputs import ChatGeneration, ChatResult
+from langchain_core.callbacks import CallbackManagerForLLMRun
+
+
+class ClaudeMaxLLM(BaseChatModel):
+ """
+ A LangChain-compatible chat model that uses Claude CLI with Max subscription.
+
+ This bypasses API key requirements by using the Claude CLI which authenticates
+ via OAuth tokens from your Claude Max subscription.
+ """
+
+ model: str = "sonnet" # Use alias for Claude Max subscription
+ max_tokens: int = 4096
+ temperature: float = 0.7
+ claude_cli_path: str = "claude"
+
+ @property
+ def _llm_type(self) -> str:
+ return "claude-max"
+
+ @property
+ def _identifying_params(self) -> Dict[str, Any]:
+ return {
+ "model": self.model,
+ "max_tokens": self.max_tokens,
+ "temperature": self.temperature,
+ }
+
+ def _format_messages_for_prompt(self, messages: List[BaseMessage]) -> str:
+ """Convert LangChain messages to a single prompt string."""
+ formatted_parts = []
+
+ for msg in messages:
+ if isinstance(msg, SystemMessage):
+ formatted_parts.append(f"\n{msg.content}\n \n")
+ elif isinstance(msg, HumanMessage):
+ formatted_parts.append(f"Human: {msg.content}\n")
+ elif isinstance(msg, AIMessage):
+ formatted_parts.append(f"Assistant: {msg.content}\n")
+ else:
+ formatted_parts.append(f"{msg.content}\n")
+
+ return "\n".join(formatted_parts)
+
+ def _call_claude_cli(self, prompt: str) -> str:
+ """Call the Claude CLI and return the response."""
+ # Create environment without ANTHROPIC_API_KEY to force subscription auth
+ env = os.environ.copy()
+ env.pop("ANTHROPIC_API_KEY", None)
+
+ # Build the command
+ cmd = [
+ self.claude_cli_path,
+ "--print", # Non-interactive mode
+ "--model", self.model,
+ prompt
+ ]
+
+ try:
+ result = subprocess.run(
+ cmd,
+ capture_output=True,
+ text=True,
+ env=env,
+ timeout=300, # 5 minute timeout
+ )
+
+ if result.returncode != 0:
+ raise RuntimeError(f"Claude CLI error: {result.stderr}")
+
+ return result.stdout.strip()
+
+ except subprocess.TimeoutExpired:
+ raise RuntimeError("Claude CLI timed out after 5 minutes")
+ except FileNotFoundError:
+ raise RuntimeError(
+ f"Claude CLI not found at '{self.claude_cli_path}'. "
+ "Make sure Claude Code is installed and in your PATH."
+ )
+
+ def _generate(
+ self,
+ messages: List[BaseMessage],
+ stop: Optional[List[str]] = None,
+ run_manager: Optional[CallbackManagerForLLMRun] = None,
+ **kwargs: Any,
+ ) -> ChatResult:
+ """Generate a response from the Claude CLI."""
+ prompt = self._format_messages_for_prompt(messages)
+ response_text = self._call_claude_cli(prompt)
+
+ # Apply stop sequences if provided
+ if stop:
+ for stop_seq in stop:
+ if stop_seq in response_text:
+ response_text = response_text.split(stop_seq)[0]
+
+ message = AIMessage(content=response_text)
+ generation = ChatGeneration(message=message)
+
+ return ChatResult(generations=[generation])
+
+ def invoke(self, input: Any, **kwargs) -> AIMessage:
+ """Invoke the model with the given input."""
+ if isinstance(input, str):
+ messages = [HumanMessage(content=input)]
+ elif isinstance(input, list):
+ messages = input
+ else:
+ messages = [HumanMessage(content=str(input))]
+
+ result = self._generate(messages, **kwargs)
+ return result.generations[0].message
+
+
+def get_claude_max_llm(model: str = "claude-sonnet-4-5-20250514", **kwargs) -> ClaudeMaxLLM:
+ """
+ Factory function to create a ClaudeMaxLLM instance.
+
+ Args:
+ model: The Claude model to use (default: claude-sonnet-4-5-20250514)
+ **kwargs: Additional arguments passed to ClaudeMaxLLM
+
+ Returns:
+ A configured ClaudeMaxLLM instance
+ """
+ return ClaudeMaxLLM(model=model, **kwargs)
+
+
+def test_claude_max():
+ """Test the Claude Max LLM wrapper."""
+ print("Testing Claude Max LLM wrapper...")
+
+ llm = ClaudeMaxLLM(model="claude-sonnet-4-5-20250514")
+
+ # Test with a simple prompt
+ response = llm.invoke("Say 'Hello, I am using Claude Max subscription!' in exactly those words.")
+ print(f"Response: {response.content}")
+
+ return response
+
+
+if __name__ == "__main__":
+ test_claude_max()
diff --git a/tradingagents/dataflows/google.py b/tradingagents/dataflows/google.py
index 3fe20f3c..bf424e71 100644
--- a/tradingagents/dataflows/google.py
+++ b/tradingagents/dataflows/google.py
@@ -1,14 +1,50 @@
-from typing import Annotated
+from typing import Annotated, Union
from datetime import datetime
from dateutil.relativedelta import relativedelta
from .googlenews_utils import getNewsData
+from .markets import is_nifty_50_stock, get_nifty_50_company_name
def get_google_news(
query: Annotated[str, "Query to search with"],
curr_date: Annotated[str, "Curr date in yyyy-mm-dd format"],
- look_back_days: Annotated[int, "how many days to look back"],
+ look_back_days: Annotated[Union[int, str], "how many days to look back OR end_date string"],
) -> str:
+ """
+ Fetch Google News for a query.
+
+ Note: This function handles two calling conventions:
+ 1. Original: (query, curr_date, look_back_days: int)
+ 2. From get_news interface: (ticker, start_date, end_date) where end_date is a string
+
+ When called with end_date string, it calculates look_back_days from the date difference.
+ """
+ # Handle case where look_back_days is actually an end_date string (from get_news interface)
+ if isinstance(look_back_days, str):
+ try:
+ # Called as (ticker, start_date, end_date) - need to swap and calculate
+ start_date_dt = datetime.strptime(curr_date, "%Y-%m-%d")
+ end_date_dt = datetime.strptime(look_back_days, "%Y-%m-%d")
+ # Swap: curr_date should be end_date, calculate days difference
+ actual_curr_date = look_back_days # end_date becomes curr_date
+ actual_look_back_days = (end_date_dt - start_date_dt).days
+ if actual_look_back_days < 0:
+ actual_look_back_days = abs(actual_look_back_days)
+ curr_date = actual_curr_date
+ look_back_days = actual_look_back_days
+ except ValueError:
+ # If parsing fails, default to 7 days
+ look_back_days = 7
+
+ # For NSE stocks, enhance query with company name for better news results
+ original_query = query
+ if is_nifty_50_stock(query):
+ company_name = get_nifty_50_company_name(query)
+ if company_name:
+ # Use company name for better news search results
+ # Add "NSE" and "stock" to filter for relevant financial news
+ query = f"{company_name} NSE stock"
+
query = query.replace(" ", "+")
start_date = datetime.strptime(curr_date, "%Y-%m-%d")
@@ -27,4 +63,6 @@ def get_google_news(
if len(news_results) == 0:
return ""
- return f"## {query} Google News, from {before} to {curr_date}:\n\n{news_str}"
\ No newline at end of file
+ # Use original query (symbol) in the header for clarity
+ display_query = original_query if is_nifty_50_stock(original_query) else query.replace("+", " ")
+ return f"## {display_query} Google News, from {before} to {curr_date}:\n\n{news_str}"
\ No newline at end of file
diff --git a/tradingagents/dataflows/interface.py b/tradingagents/dataflows/interface.py
index 4cd5ddef..67d0604a 100644
--- a/tradingagents/dataflows/interface.py
+++ b/tradingagents/dataflows/interface.py
@@ -16,6 +16,8 @@ from .alpha_vantage import (
get_news as get_alpha_vantage_news
)
from .alpha_vantage_common import AlphaVantageRateLimitError
+from .jugaad_data import get_jugaad_stock_data, get_jugaad_indicators
+from .markets import detect_market, Market, is_nifty_50_stock
# Configuration and routing logic
from .config import get_config
@@ -58,7 +60,8 @@ VENDOR_LIST = [
"local",
"yfinance",
"openai",
- "google"
+ "google",
+ "jugaad_data"
]
# Mapping of methods to their vendor-specific implementations
@@ -68,12 +71,14 @@ VENDOR_METHODS = {
"alpha_vantage": get_alpha_vantage_stock,
"yfinance": get_YFin_data_online,
"local": get_YFin_data,
+ "jugaad_data": get_jugaad_stock_data,
},
# technical_indicators
"get_indicators": {
"alpha_vantage": get_alpha_vantage_indicator,
"yfinance": get_stock_stats_indicators_window,
- "local": get_stock_stats_indicators_window
+ "local": get_stock_stats_indicators_window,
+ "jugaad_data": get_jugaad_indicators,
},
# fundamental_data
"get_fundamentals": {
@@ -123,9 +128,18 @@ def get_category_for_method(method: str) -> str:
return category
raise ValueError(f"Method '{method}' not found in any category")
-def get_vendor(category: str, method: str = None) -> str:
+def get_vendor(category: str, method: str = None, symbol: str = None) -> str:
"""Get the configured vendor for a data category or specific tool method.
Tool-level configuration takes precedence over category-level.
+ For NSE stocks, automatically routes to jugaad_data for core_stock_apis and technical_indicators.
+
+ Args:
+ category: Data category (e.g., "core_stock_apis", "technical_indicators")
+ method: Specific tool method name
+ symbol: Stock symbol (used for market detection)
+
+ Returns:
+ Vendor name string
"""
config = get_config()
@@ -135,13 +149,44 @@ def get_vendor(category: str, method: str = None) -> str:
if method in tool_vendors:
return tool_vendors[method]
+ # Market-aware vendor routing for NSE stocks
+ if symbol:
+ market_config = config.get("market", "auto")
+ market = detect_market(symbol, market_config)
+
+ if market == Market.INDIA_NSE:
+ # Use yfinance as primary for NSE stocks (more reliable than jugaad_data from outside India)
+ # jugaad_data requires direct NSE access which may be blocked/slow
+ if category in ("core_stock_apis", "technical_indicators"):
+ return "yfinance" # yfinance handles .NS suffix automatically
+ # Use yfinance for fundamentals (with .NS suffix handled in y_finance.py)
+ elif category == "fundamental_data":
+ return "yfinance"
+ # Use google for news (handled in google.py with company name enhancement)
+ elif category == "news_data":
+ return "google"
+
# Fall back to category-level configuration
return config.get("data_vendors", {}).get(category, "default")
def route_to_vendor(method: str, *args, **kwargs):
"""Route method calls to appropriate vendor implementation with fallback support."""
category = get_category_for_method(method)
- vendor_config = get_vendor(category, method)
+
+ # Extract symbol from args/kwargs for market-aware routing
+ symbol = None
+ if args:
+ # First argument is typically the symbol/ticker
+ symbol = args[0]
+ elif "symbol" in kwargs:
+ symbol = kwargs["symbol"]
+ elif "ticker" in kwargs:
+ symbol = kwargs["ticker"]
+ elif "query" in kwargs:
+ # For news queries, the query might be the symbol
+ symbol = kwargs["query"]
+
+ vendor_config = get_vendor(category, method, symbol)
# Handle comma-separated vendors
primary_vendors = [v.strip() for v in vendor_config.split(',')]
diff --git a/tradingagents/dataflows/jugaad_data.py b/tradingagents/dataflows/jugaad_data.py
new file mode 100644
index 00000000..e51835fc
--- /dev/null
+++ b/tradingagents/dataflows/jugaad_data.py
@@ -0,0 +1,372 @@
+"""
+Data vendor using jugaad-data library for Indian NSE stocks.
+Provides historical OHLCV data, live quotes, and index data for NSE.
+
+Note: jugaad-data requires network access to NSE India website which may be
+slow or blocked from some locations. The implementation includes timeouts
+and will raise exceptions to trigger fallback to yfinance.
+"""
+
+from typing import Annotated
+from datetime import datetime, date
+import pandas as pd
+import signal
+
+from .markets import normalize_symbol, is_nifty_50_stock
+
+
+class JugaadDataTimeoutError(Exception):
+ """Raised when jugaad-data request times out."""
+ pass
+
+
+def _timeout_handler(signum, frame):
+ raise JugaadDataTimeoutError("jugaad-data request timed out")
+
+
+def get_jugaad_stock_data(
+ symbol: Annotated[str, "ticker symbol of the company"],
+ start_date: Annotated[str, "Start date in yyyy-mm-dd format"],
+ end_date: Annotated[str, "End date in yyyy-mm-dd format"],
+) -> str:
+ """
+ Fetch historical stock data from NSE using jugaad-data.
+
+ Args:
+ symbol: NSE stock symbol (e.g., 'RELIANCE', 'TCS')
+ start_date: Start date in yyyy-mm-dd format
+ end_date: End date in yyyy-mm-dd format
+
+ Returns:
+ CSV formatted string with OHLCV data
+
+ Raises:
+ ImportError: If jugaad-data is not installed
+ JugaadDataTimeoutError: If request times out
+ Exception: For other errors (triggers fallback)
+ """
+ try:
+ from jugaad_data.nse import stock_df
+ except ImportError:
+ raise ImportError("jugaad-data library not installed. Please install it with: pip install jugaad-data")
+
+ # Normalize symbol for NSE (remove .NS suffix if present)
+ nse_symbol = normalize_symbol(symbol, target="nse")
+
+ # Parse dates
+ try:
+ start_dt = datetime.strptime(start_date, "%Y-%m-%d").date()
+ end_dt = datetime.strptime(end_date, "%Y-%m-%d").date()
+ except ValueError as e:
+ raise ValueError(f"Error parsing dates: {e}. Please use yyyy-mm-dd format.")
+
+ # Set a timeout for the request (15 seconds)
+ # This helps avoid hanging when NSE website is slow
+ timeout_seconds = 15
+ old_handler = None
+
+ try:
+ # Set timeout using signal (only works on Unix)
+ try:
+ old_handler = signal.signal(signal.SIGALRM, _timeout_handler)
+ signal.alarm(timeout_seconds)
+ except (AttributeError, ValueError):
+ # signal.SIGALRM not available on Windows
+ pass
+
+ # Fetch data using jugaad-data
+ # series='EQ' for equity stocks
+ data = stock_df(
+ symbol=nse_symbol,
+ from_date=start_dt,
+ to_date=end_dt,
+ series="EQ"
+ )
+
+ # Cancel the alarm
+ try:
+ signal.alarm(0)
+ if old_handler:
+ signal.signal(signal.SIGALRM, old_handler)
+ except (AttributeError, ValueError):
+ pass
+
+ if data.empty:
+ raise ValueError(f"No data found for symbol '{nse_symbol}' between {start_date} and {end_date}")
+
+ # Rename columns to match yfinance format for consistency
+ column_mapping = {
+ "DATE": "Date",
+ "OPEN": "Open",
+ "HIGH": "High",
+ "LOW": "Low",
+ "CLOSE": "Close",
+ "LTP": "Last",
+ "VOLUME": "Volume",
+ "VALUE": "Value",
+ "NO OF TRADES": "Trades",
+ "PREV. CLOSE": "Prev Close",
+ }
+
+ # Rename columns that exist
+ for old_name, new_name in column_mapping.items():
+ if old_name in data.columns:
+ data = data.rename(columns={old_name: new_name})
+
+ # Select relevant columns (similar to yfinance output)
+ available_cols = ["Date", "Open", "High", "Low", "Close", "Volume"]
+ cols_to_use = [col for col in available_cols if col in data.columns]
+ data = data[cols_to_use]
+
+ # Round numerical values
+ numeric_columns = ["Open", "High", "Low", "Close"]
+ for col in numeric_columns:
+ if col in data.columns:
+ data[col] = data[col].round(2)
+
+ # Sort by date
+ if "Date" in data.columns:
+ data = data.sort_values("Date")
+
+ # Convert to CSV string
+ csv_string = data.to_csv(index=False)
+
+ # Add header information
+ header = f"# Stock data for {nse_symbol} (NSE) from {start_date} to {end_date}\n"
+ header += f"# Total records: {len(data)}\n"
+ header += f"# Data source: NSE India via jugaad-data\n"
+ header += f"# Data retrieved on: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}\n\n"
+
+ return header + csv_string
+
+ except JugaadDataTimeoutError:
+ # Re-raise timeout errors to trigger fallback
+ raise
+ except Exception as e:
+ error_msg = str(e)
+ # Raise exceptions to trigger fallback to yfinance
+ if "No data" in error_msg or "empty" in error_msg.lower():
+ raise ValueError(f"No data found for symbol '{nse_symbol}' between {start_date} and {end_date}. Please verify the symbol is listed on NSE.")
+ raise RuntimeError(f"Error fetching data for {nse_symbol} from jugaad-data: {error_msg}")
+
+
+def get_jugaad_live_quote(
+ symbol: Annotated[str, "ticker symbol of the company"],
+) -> str:
+ """
+ Fetch live quote for an NSE stock.
+
+ Args:
+ symbol: NSE stock symbol
+
+ Returns:
+ Formatted string with current quote information
+ """
+ try:
+ from jugaad_data.nse import NSELive
+ except ImportError:
+ return "Error: jugaad-data library not installed. Please install it with: pip install jugaad-data"
+
+ nse_symbol = normalize_symbol(symbol, target="nse")
+
+ try:
+ nse = NSELive()
+ quote = nse.stock_quote(nse_symbol)
+
+ if not quote:
+ return f"No live quote available for '{nse_symbol}'"
+
+ # Extract price info
+ price_info = quote.get("priceInfo", {})
+ trade_info = quote.get("tradeInfo", {})
+ security_info = quote.get("securityInfo", {})
+
+ result = f"# Live Quote for {nse_symbol} (NSE)\n"
+ result += f"# Retrieved: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}\n\n"
+
+ result += f"Last Price: {price_info.get('lastPrice', 'N/A')}\n"
+ result += f"Change: {price_info.get('change', 'N/A')}\n"
+ result += f"% Change: {price_info.get('pChange', 'N/A')}%\n"
+ result += f"Open: {price_info.get('open', 'N/A')}\n"
+ result += f"High: {price_info.get('intraDayHighLow', {}).get('max', 'N/A')}\n"
+ result += f"Low: {price_info.get('intraDayHighLow', {}).get('min', 'N/A')}\n"
+ result += f"Previous Close: {price_info.get('previousClose', 'N/A')}\n"
+ result += f"Volume: {trade_info.get('totalTradedVolume', 'N/A')}\n"
+ result += f"Value: {trade_info.get('totalTradedValue', 'N/A')}\n"
+
+ return result
+
+ except Exception as e:
+ return f"Error fetching live quote for {nse_symbol}: {str(e)}"
+
+
+def get_jugaad_index_data(
+ index_name: Annotated[str, "Index name (e.g., 'NIFTY 50', 'NIFTY BANK')"],
+ start_date: Annotated[str, "Start date in yyyy-mm-dd format"],
+ end_date: Annotated[str, "End date in yyyy-mm-dd format"],
+) -> str:
+ """
+ Fetch historical index data from NSE.
+
+ Args:
+ index_name: NSE index name (e.g., 'NIFTY 50', 'NIFTY BANK')
+ start_date: Start date in yyyy-mm-dd format
+ end_date: End date in yyyy-mm-dd format
+
+ Returns:
+ CSV formatted string with index data
+ """
+ try:
+ from jugaad_data.nse import index_df
+ except ImportError:
+ return "Error: jugaad-data library not installed. Please install it with: pip install jugaad-data"
+
+ try:
+ start_dt = datetime.strptime(start_date, "%Y-%m-%d").date()
+ end_dt = datetime.strptime(end_date, "%Y-%m-%d").date()
+ except ValueError as e:
+ return f"Error parsing dates: {e}. Please use yyyy-mm-dd format."
+
+ try:
+ data = index_df(
+ symbol=index_name.upper(),
+ from_date=start_dt,
+ to_date=end_dt
+ )
+
+ if data.empty:
+ return f"No data found for index '{index_name}' between {start_date} and {end_date}"
+
+ # Sort by date
+ if "HistoricalDate" in data.columns:
+ data = data.sort_values("HistoricalDate")
+
+ csv_string = data.to_csv(index=False)
+
+ header = f"# Index data for {index_name} from {start_date} to {end_date}\n"
+ header += f"# Total records: {len(data)}\n"
+ header += f"# Data source: NSE India via jugaad-data\n"
+ header += f"# Data retrieved on: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}\n\n"
+
+ return header + csv_string
+
+ except Exception as e:
+ return f"Error fetching index data for {index_name}: {str(e)}"
+
+
+def get_jugaad_indicators(
+ symbol: Annotated[str, "ticker symbol of the company"],
+ indicator: Annotated[str, "technical indicator to calculate"],
+ curr_date: Annotated[str, "The current trading date, YYYY-mm-dd"],
+ look_back_days: Annotated[int, "how many days to look back"] = 30,
+) -> str:
+ """
+ Calculate technical indicators for NSE stocks using jugaad-data.
+ This fetches data and calculates indicators using stockstats.
+
+ Args:
+ symbol: NSE stock symbol
+ indicator: Technical indicator name
+ curr_date: Current date for calculation
+ look_back_days: Number of days to look back
+
+ Returns:
+ Formatted string with indicator values
+
+ Raises:
+ ImportError: If required libraries not installed
+ Exception: For other errors (triggers fallback)
+ """
+ try:
+ from jugaad_data.nse import stock_df
+ from stockstats import wrap
+ except ImportError as e:
+ raise ImportError(f"Required library not installed: {e}")
+
+ nse_symbol = normalize_symbol(symbol, target="nse")
+
+ # Set timeout for NSE request
+ timeout_seconds = 15
+ old_handler = None
+
+ try:
+ # Set timeout using signal (only works on Unix)
+ try:
+ old_handler = signal.signal(signal.SIGALRM, _timeout_handler)
+ signal.alarm(timeout_seconds)
+ except (AttributeError, ValueError):
+ pass
+
+ # Calculate date range - need more history for indicator calculation
+ curr_dt = datetime.strptime(curr_date, "%Y-%m-%d").date()
+ # Fetch extra data for indicator calculation (e.g., 200-day SMA needs 200+ days)
+ start_dt = date(curr_dt.year - 1, curr_dt.month, curr_dt.day) # 1 year back
+
+ data = stock_df(
+ symbol=nse_symbol,
+ from_date=start_dt,
+ to_date=curr_dt,
+ series="EQ"
+ )
+
+ # Cancel the alarm
+ try:
+ signal.alarm(0)
+ if old_handler:
+ signal.signal(signal.SIGALRM, old_handler)
+ except (AttributeError, ValueError):
+ pass
+
+ if data.empty:
+ raise ValueError(f"No data found for symbol '{nse_symbol}' to calculate {indicator}")
+
+ # Prepare data for stockstats
+ column_mapping = {
+ "DATE": "date",
+ "OPEN": "open",
+ "HIGH": "high",
+ "LOW": "low",
+ "CLOSE": "close",
+ "VOLUME": "volume",
+ }
+
+ for old_name, new_name in column_mapping.items():
+ if old_name in data.columns:
+ data = data.rename(columns={old_name: new_name})
+
+ # Wrap with stockstats
+ df = wrap(data)
+
+ # Calculate the indicator
+ df[indicator] # This triggers stockstats calculation
+
+ # Get the last N days of indicator values
+ from dateutil.relativedelta import relativedelta
+ result_data = []
+ curr_date_dt = datetime.strptime(curr_date, "%Y-%m-%d")
+ before = curr_date_dt - relativedelta(days=look_back_days)
+
+ df["date_str"] = pd.to_datetime(df["date"]).dt.strftime("%Y-%m-%d")
+
+ for _, row in df.iterrows():
+ row_date = datetime.strptime(row["date_str"], "%Y-%m-%d")
+ if before <= row_date <= curr_date_dt:
+ ind_value = row[indicator]
+ if pd.isna(ind_value):
+ result_data.append((row["date_str"], "N/A"))
+ else:
+ result_data.append((row["date_str"], str(round(ind_value, 4))))
+
+ result_data.sort(reverse=True) # Most recent first
+
+ result_str = f"## {indicator} values for {nse_symbol} (NSE) from {before.strftime('%Y-%m-%d')} to {curr_date}:\n\n"
+ for date_str, value in result_data:
+ result_str += f"{date_str}: {value}\n"
+
+ return result_str
+
+ except JugaadDataTimeoutError:
+ # Re-raise timeout to trigger fallback
+ raise
+ except Exception as e:
+ raise RuntimeError(f"Error calculating {indicator} for {nse_symbol}: {str(e)}")
diff --git a/tradingagents/dataflows/markets.py b/tradingagents/dataflows/markets.py
new file mode 100644
index 00000000..e5994bb1
--- /dev/null
+++ b/tradingagents/dataflows/markets.py
@@ -0,0 +1,172 @@
+"""
+Market configuration and stock lists for different markets.
+Supports US and Indian NSE (Nifty 50) stocks.
+"""
+
+from enum import Enum
+from typing import Optional
+
+
+class Market(Enum):
+ """Supported markets."""
+ US = "us"
+ INDIA_NSE = "india_nse"
+
+
+# Nifty 50 stocks with company names
+NIFTY_50_STOCKS = {
+ "RELIANCE": "Reliance Industries Ltd",
+ "TCS": "Tata Consultancy Services Ltd",
+ "HDFCBANK": "HDFC Bank Ltd",
+ "INFY": "Infosys Ltd",
+ "ICICIBANK": "ICICI Bank Ltd",
+ "HINDUNILVR": "Hindustan Unilever Ltd",
+ "ITC": "ITC Ltd",
+ "SBIN": "State Bank of India",
+ "BHARTIARTL": "Bharti Airtel Ltd",
+ "KOTAKBANK": "Kotak Mahindra Bank Ltd",
+ "LT": "Larsen & Toubro Ltd",
+ "AXISBANK": "Axis Bank Ltd",
+ "ASIANPAINT": "Asian Paints Ltd",
+ "MARUTI": "Maruti Suzuki India Ltd",
+ "HCLTECH": "HCL Technologies Ltd",
+ "SUNPHARMA": "Sun Pharmaceutical Industries Ltd",
+ "TITAN": "Titan Company Ltd",
+ "BAJFINANCE": "Bajaj Finance Ltd",
+ "WIPRO": "Wipro Ltd",
+ "ULTRACEMCO": "UltraTech Cement Ltd",
+ "NESTLEIND": "Nestle India Ltd",
+ "NTPC": "NTPC Ltd",
+ "POWERGRID": "Power Grid Corporation of India Ltd",
+ "M&M": "Mahindra & Mahindra Ltd",
+ "TATAMOTORS": "Tata Motors Ltd",
+ "ONGC": "Oil & Natural Gas Corporation Ltd",
+ "JSWSTEEL": "JSW Steel Ltd",
+ "TATASTEEL": "Tata Steel Ltd",
+ "ADANIENT": "Adani Enterprises Ltd",
+ "ADANIPORTS": "Adani Ports and SEZ Ltd",
+ "COALINDIA": "Coal India Ltd",
+ "BAJAJFINSV": "Bajaj Finserv Ltd",
+ "TECHM": "Tech Mahindra Ltd",
+ "HDFCLIFE": "HDFC Life Insurance Company Ltd",
+ "SBILIFE": "SBI Life Insurance Company Ltd",
+ "GRASIM": "Grasim Industries Ltd",
+ "DIVISLAB": "Divi's Laboratories Ltd",
+ "DRREDDY": "Dr. Reddy's Laboratories Ltd",
+ "CIPLA": "Cipla Ltd",
+ "BRITANNIA": "Britannia Industries Ltd",
+ "EICHERMOT": "Eicher Motors Ltd",
+ "APOLLOHOSP": "Apollo Hospitals Enterprise Ltd",
+ "INDUSINDBK": "IndusInd Bank Ltd",
+ "HEROMOTOCO": "Hero MotoCorp Ltd",
+ "TATACONSUM": "Tata Consumer Products Ltd",
+ "BPCL": "Bharat Petroleum Corporation Ltd",
+ "UPL": "UPL Ltd",
+ "HINDALCO": "Hindalco Industries Ltd",
+ "BAJAJ-AUTO": "Bajaj Auto Ltd",
+ "LTIM": "LTIMindtree Ltd",
+}
+
+
+def is_nifty_50_stock(symbol: str) -> bool:
+ """
+ Check if a symbol is a Nifty 50 stock.
+
+ Args:
+ symbol: Stock symbol (with or without .NS suffix)
+
+ Returns:
+ True if the symbol is in the Nifty 50 list
+ """
+ # Remove .NS suffix if present
+ clean_symbol = symbol.upper().replace(".NS", "")
+ return clean_symbol in NIFTY_50_STOCKS
+
+
+def get_nifty_50_company_name(symbol: str) -> Optional[str]:
+ """
+ Get the company name for a Nifty 50 stock symbol.
+
+ Args:
+ symbol: Stock symbol (with or without .NS suffix)
+
+ Returns:
+ Company name if found, None otherwise
+ """
+ clean_symbol = symbol.upper().replace(".NS", "")
+ return NIFTY_50_STOCKS.get(clean_symbol)
+
+
+def detect_market(symbol: str, config_market: str = "auto") -> Market:
+ """
+ Detect the market for a given symbol.
+
+ Args:
+ symbol: Stock symbol
+ config_market: Market setting from config ("auto", "us", "india_nse")
+
+ Returns:
+ Market enum indicating the detected market
+ """
+ if config_market == "india_nse":
+ return Market.INDIA_NSE
+ elif config_market == "us":
+ return Market.US
+
+ # Auto-detection
+ # Check if symbol has .NS suffix (yfinance format for NSE)
+ if symbol.upper().endswith(".NS"):
+ return Market.INDIA_NSE
+
+ # Check if symbol is in Nifty 50 list
+ if is_nifty_50_stock(symbol):
+ return Market.INDIA_NSE
+
+ # Default to US market
+ return Market.US
+
+
+def normalize_symbol(symbol: str, target: str = "yfinance") -> str:
+ """
+ Normalize a symbol for a specific data source.
+
+ Args:
+ symbol: Stock symbol
+ target: Target format ("yfinance", "jugaad", "nse")
+
+ Returns:
+ Normalized symbol for the target
+ """
+ clean_symbol = symbol.upper().replace(".NS", "")
+
+ if target == "yfinance":
+ # yfinance requires .NS suffix for NSE stocks
+ if is_nifty_50_stock(clean_symbol):
+ return f"{clean_symbol}.NS"
+ return clean_symbol
+
+ elif target in ("jugaad", "nse"):
+ # jugaad-data and NSE use symbols without suffix
+ return clean_symbol
+
+ return symbol.upper()
+
+
+def get_nifty_50_list() -> list:
+ """
+ Get list of all Nifty 50 stock symbols.
+
+ Returns:
+ List of Nifty 50 stock symbols
+ """
+ return list(NIFTY_50_STOCKS.keys())
+
+
+def get_nifty_50_with_names() -> dict:
+ """
+ Get dictionary of Nifty 50 stocks with company names.
+
+ Returns:
+ Dictionary mapping symbols to company names
+ """
+ return NIFTY_50_STOCKS.copy()
diff --git a/tradingagents/dataflows/y_finance.py b/tradingagents/dataflows/y_finance.py
index da7273d5..b6f109b3 100644
--- a/tradingagents/dataflows/y_finance.py
+++ b/tradingagents/dataflows/y_finance.py
@@ -4,6 +4,7 @@ from dateutil.relativedelta import relativedelta
import yfinance as yf
import os
from .stockstats_utils import StockstatsUtils
+from .markets import normalize_symbol, is_nifty_50_stock
def get_YFin_data_online(
symbol: Annotated[str, "ticker symbol of the company"],
@@ -14,8 +15,11 @@ def get_YFin_data_online(
datetime.strptime(start_date, "%Y-%m-%d")
datetime.strptime(end_date, "%Y-%m-%d")
+ # Normalize symbol for yfinance (adds .NS suffix for NSE stocks)
+ normalized_symbol = normalize_symbol(symbol, target="yfinance")
+
# Create ticker object
- ticker = yf.Ticker(symbol.upper())
+ ticker = yf.Ticker(normalized_symbol)
# Fetch historical data for the specified date range
data = ticker.history(start=start_date, end=end_date)
@@ -23,7 +27,7 @@ def get_YFin_data_online(
# Check if data is empty
if data.empty:
return (
- f"No data found for symbol '{symbol}' between {start_date} and {end_date}"
+ f"No data found for symbol '{normalized_symbol}' between {start_date} and {end_date}"
)
# Remove timezone info from index for cleaner output
@@ -40,7 +44,7 @@ def get_YFin_data_online(
csv_string = data.to_csv()
# Add header information
- header = f"# Stock data for {symbol.upper()} from {start_date} to {end_date}\n"
+ header = f"# Stock data for {normalized_symbol} from {start_date} to {end_date}\n"
header += f"# Total records: {len(data)}\n"
header += f"# Data retrieved on: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}\n\n"
@@ -300,7 +304,9 @@ def get_balance_sheet(
):
"""Get balance sheet data from yfinance."""
try:
- ticker_obj = yf.Ticker(ticker.upper())
+ # Normalize symbol for yfinance (adds .NS suffix for NSE stocks)
+ normalized_ticker = normalize_symbol(ticker, target="yfinance")
+ ticker_obj = yf.Ticker(normalized_ticker)
if freq.lower() == "quarterly":
data = ticker_obj.quarterly_balance_sheet
@@ -308,19 +314,19 @@ def get_balance_sheet(
data = ticker_obj.balance_sheet
if data.empty:
- return f"No balance sheet data found for symbol '{ticker}'"
-
+ return f"No balance sheet data found for symbol '{normalized_ticker}'"
+
# Convert to CSV string for consistency with other functions
csv_string = data.to_csv()
-
+
# Add header information
- header = f"# Balance Sheet data for {ticker.upper()} ({freq})\n"
+ header = f"# Balance Sheet data for {normalized_ticker} ({freq})\n"
header += f"# Data retrieved on: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}\n\n"
-
+
return header + csv_string
-
+
except Exception as e:
- return f"Error retrieving balance sheet for {ticker}: {str(e)}"
+ return f"Error retrieving balance sheet for {normalized_ticker}: {str(e)}"
def get_cashflow(
@@ -330,27 +336,29 @@ def get_cashflow(
):
"""Get cash flow data from yfinance."""
try:
- ticker_obj = yf.Ticker(ticker.upper())
-
+ # Normalize symbol for yfinance (adds .NS suffix for NSE stocks)
+ normalized_ticker = normalize_symbol(ticker, target="yfinance")
+ ticker_obj = yf.Ticker(normalized_ticker)
+
if freq.lower() == "quarterly":
data = ticker_obj.quarterly_cashflow
else:
data = ticker_obj.cashflow
-
+
if data.empty:
- return f"No cash flow data found for symbol '{ticker}'"
-
+ return f"No cash flow data found for symbol '{normalized_ticker}'"
+
# Convert to CSV string for consistency with other functions
csv_string = data.to_csv()
-
+
# Add header information
- header = f"# Cash Flow data for {ticker.upper()} ({freq})\n"
+ header = f"# Cash Flow data for {normalized_ticker} ({freq})\n"
header += f"# Data retrieved on: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}\n\n"
-
+
return header + csv_string
-
+
except Exception as e:
- return f"Error retrieving cash flow for {ticker}: {str(e)}"
+ return f"Error retrieving cash flow for {normalized_ticker}: {str(e)}"
def get_income_statement(
@@ -360,27 +368,29 @@ def get_income_statement(
):
"""Get income statement data from yfinance."""
try:
- ticker_obj = yf.Ticker(ticker.upper())
-
+ # Normalize symbol for yfinance (adds .NS suffix for NSE stocks)
+ normalized_ticker = normalize_symbol(ticker, target="yfinance")
+ ticker_obj = yf.Ticker(normalized_ticker)
+
if freq.lower() == "quarterly":
data = ticker_obj.quarterly_income_stmt
else:
data = ticker_obj.income_stmt
-
+
if data.empty:
- return f"No income statement data found for symbol '{ticker}'"
-
+ return f"No income statement data found for symbol '{normalized_ticker}'"
+
# Convert to CSV string for consistency with other functions
csv_string = data.to_csv()
-
+
# Add header information
- header = f"# Income Statement data for {ticker.upper()} ({freq})\n"
+ header = f"# Income Statement data for {normalized_ticker} ({freq})\n"
header += f"# Data retrieved on: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}\n\n"
-
+
return header + csv_string
-
+
except Exception as e:
- return f"Error retrieving income statement for {ticker}: {str(e)}"
+ return f"Error retrieving income statement for {normalized_ticker}: {str(e)}"
def get_insider_transactions(
@@ -388,20 +398,26 @@ def get_insider_transactions(
):
"""Get insider transactions data from yfinance."""
try:
- ticker_obj = yf.Ticker(ticker.upper())
+ # Normalize symbol for yfinance (adds .NS suffix for NSE stocks)
+ normalized_ticker = normalize_symbol(ticker, target="yfinance")
+ ticker_obj = yf.Ticker(normalized_ticker)
data = ticker_obj.insider_transactions
-
+
if data is None or data.empty:
- return f"No insider transactions data found for symbol '{ticker}'"
-
+ # Check if this is an NSE stock - insider data may not be available
+ if is_nifty_50_stock(ticker):
+ return (f"Note: SEC-style insider transaction data is not available for Indian NSE stocks like {normalized_ticker}. "
+ f"For Indian stocks, insider trading disclosures are filed with SEBI and available through NSE/BSE websites.")
+ return f"No insider transactions data found for symbol '{normalized_ticker}'"
+
# Convert to CSV string for consistency with other functions
csv_string = data.to_csv()
-
+
# Add header information
- header = f"# Insider Transactions data for {ticker.upper()}\n"
+ header = f"# Insider Transactions data for {normalized_ticker}\n"
header += f"# Data retrieved on: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}\n\n"
-
+
return header + csv_string
-
+
except Exception as e:
- return f"Error retrieving insider transactions for {ticker}: {str(e)}"
\ No newline at end of file
+ return f"Error retrieving insider transactions for {normalized_ticker}: {str(e)}"
\ No newline at end of file
diff --git a/tradingagents/default_config.py b/tradingagents/default_config.py
index 1f40a2a2..57adff38 100644
--- a/tradingagents/default_config.py
+++ b/tradingagents/default_config.py
@@ -13,16 +13,30 @@ DEFAULT_CONFIG = {
"deep_think_llm": "o4-mini",
"quick_think_llm": "gpt-4o-mini",
"backend_url": "https://api.openai.com/v1",
+ # Anthropic-specific config for Claude models (using aliases for Claude Max subscription)
+ "anthropic_config": {
+ "deep_think_llm": "opus", # Claude Opus 4.5 for deep analysis
+ "quick_think_llm": "sonnet", # Claude Sonnet 4.5 for quick tasks
+ },
# Debate and discussion settings
"max_debate_rounds": 1,
"max_risk_discuss_rounds": 1,
"max_recur_limit": 100,
+ # Market configuration
+ # Options: "auto" (detect from symbol), "us", "india_nse"
+ # When set to "auto", Nifty 50 stocks are automatically detected and routed to NSE vendors
+ "market": "auto",
# Data vendor configuration
# Category-level configuration (default for all tools in category)
+ # For NSE stocks (when market is "auto" or "india_nse"):
+ # - core_stock_apis: jugaad_data (primary), yfinance (fallback)
+ # - technical_indicators: jugaad_data (primary), yfinance (fallback)
+ # - fundamental_data: yfinance (with .NS suffix)
+ # - news_data: google (with company name enhancement)
"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
+ "core_stock_apis": "yfinance", # Options: yfinance, alpha_vantage, local, jugaad_data
+ "technical_indicators": "yfinance", # Options: yfinance, alpha_vantage, local, jugaad_data
+ "fundamental_data": "alpha_vantage", # Options: openai, alpha_vantage, local, yfinance
"news_data": "alpha_vantage", # Options: openai, alpha_vantage, google, local
},
# Tool-level configuration (takes precedence over category-level)
diff --git a/tradingagents/graph/trading_graph.py b/tradingagents/graph/trading_graph.py
index 40cdff75..f1866d93 100644
--- a/tradingagents/graph/trading_graph.py
+++ b/tradingagents/graph/trading_graph.py
@@ -9,6 +9,7 @@ from typing import Dict, Any, Tuple, List, Optional
from langchain_openai import ChatOpenAI
from langchain_anthropic import ChatAnthropic
from langchain_google_genai import ChatGoogleGenerativeAI
+from tradingagents.claude_max_llm import ClaudeMaxLLM
from langgraph.prebuilt import ToolNode
@@ -76,8 +77,9 @@ class TradingAgentsGraph:
self.deep_thinking_llm = ChatOpenAI(model=self.config["deep_think_llm"], base_url=self.config["backend_url"])
self.quick_thinking_llm = ChatOpenAI(model=self.config["quick_think_llm"], base_url=self.config["backend_url"])
elif self.config["llm_provider"].lower() == "anthropic":
- self.deep_thinking_llm = ChatAnthropic(model=self.config["deep_think_llm"], base_url=self.config["backend_url"])
- self.quick_thinking_llm = ChatAnthropic(model=self.config["quick_think_llm"], base_url=self.config["backend_url"])
+ # Use ClaudeMaxLLM to leverage Claude Max subscription via CLI
+ self.deep_thinking_llm = ClaudeMaxLLM(model=self.config["deep_think_llm"])
+ self.quick_thinking_llm = ClaudeMaxLLM(model=self.config["quick_think_llm"])
elif self.config["llm_provider"].lower() == "google":
self.deep_thinking_llm = ChatGoogleGenerativeAI(model=self.config["deep_think_llm"])
self.quick_thinking_llm = ChatGoogleGenerativeAI(model=self.config["quick_think_llm"])
diff --git a/tradingagents/nifty50_recommender.py b/tradingagents/nifty50_recommender.py
new file mode 100644
index 00000000..15711274
--- /dev/null
+++ b/tradingagents/nifty50_recommender.py
@@ -0,0 +1,448 @@
+"""
+Nifty 50 Stock Recommendation System.
+
+This module predicts all 50 Nifty stocks and selects the ones with highest
+short-term growth potential using Claude Opus 4.5 via Claude Max subscription.
+"""
+
+import os
+# Disable CUDA to avoid library issues with embeddings
+os.environ["CUDA_VISIBLE_DEVICES"] = ""
+# Remove API key to force Claude Max subscription auth
+os.environ.pop("ANTHROPIC_API_KEY", None)
+
+import json
+from pathlib import Path
+from typing import Dict, Any, Optional, List, Tuple
+from datetime import datetime
+
+from tradingagents.graph.trading_graph import TradingAgentsGraph
+from tradingagents.default_config import DEFAULT_CONFIG
+from tradingagents.dataflows.markets import NIFTY_50_STOCKS, get_nifty_50_list
+from tradingagents.claude_max_llm import ClaudeMaxLLM
+
+
+def verify_claude_cli() -> bool:
+ """
+ Verify that Claude CLI is available and authenticated.
+
+ Returns:
+ True if Claude CLI is available
+
+ Raises:
+ RuntimeError: If Claude CLI is not available or not authenticated
+ """
+ import subprocess
+
+ try:
+ result = subprocess.run(
+ ["claude", "--version"],
+ capture_output=True,
+ text=True,
+ timeout=10
+ )
+ if result.returncode == 0:
+ return True
+ except FileNotFoundError:
+ pass
+
+ raise RuntimeError(
+ "Claude CLI is not available.\n\n"
+ "To use this recommendation system with Claude Max subscription:\n"
+ "1. Install Claude Code: npm install -g @anthropic-ai/claude-code\n"
+ "2. Authenticate: claude auth login\n"
+ )
+
+
+def create_claude_config() -> Dict[str, Any]:
+ """
+ Create a configuration dictionary for using Claude models.
+
+ Returns:
+ Configuration dictionary with Anthropic settings
+ """
+ config = DEFAULT_CONFIG.copy()
+ config["llm_provider"] = "anthropic"
+ config["deep_think_llm"] = config["anthropic_config"]["deep_think_llm"]
+ config["quick_think_llm"] = config["anthropic_config"]["quick_think_llm"]
+ config["market"] = "india_nse"
+
+ # Use jugaad_data for NSE stocks
+ config["data_vendors"] = {
+ "core_stock_apis": "jugaad_data",
+ "technical_indicators": "jugaad_data",
+ "fundamental_data": "yfinance",
+ "news_data": "google",
+ }
+
+ return config
+
+
+def predict_stock(
+ graph: TradingAgentsGraph,
+ symbol: str,
+ trade_date: str
+) -> Dict[str, Any]:
+ """
+ Run prediction for a single stock.
+
+ Args:
+ graph: The TradingAgentsGraph instance
+ symbol: Stock symbol (e.g., 'RELIANCE', 'TCS')
+ trade_date: Date for the prediction (YYYY-MM-DD format)
+
+ Returns:
+ Dictionary containing prediction results including decision,
+ market report, fundamentals report, news report, investment plan,
+ and final trade decision
+ """
+ try:
+ final_state, decision = graph.propagate(symbol, trade_date)
+
+ return {
+ "symbol": symbol,
+ "company_name": NIFTY_50_STOCKS.get(symbol, symbol),
+ "decision": decision,
+ "market_report": final_state.get("market_report", ""),
+ "fundamentals_report": final_state.get("fundamentals_report", ""),
+ "news_report": final_state.get("news_report", ""),
+ "sentiment_report": final_state.get("sentiment_report", ""),
+ "investment_plan": final_state.get("investment_plan", ""),
+ "final_trade_decision": final_state.get("final_trade_decision", ""),
+ "investment_debate": {
+ "bull_history": final_state.get("investment_debate_state", {}).get("bull_history", ""),
+ "bear_history": final_state.get("investment_debate_state", {}).get("bear_history", ""),
+ "judge_decision": final_state.get("investment_debate_state", {}).get("judge_decision", ""),
+ },
+ "risk_debate": {
+ "risky_history": final_state.get("risk_debate_state", {}).get("risky_history", ""),
+ "safe_history": final_state.get("risk_debate_state", {}).get("safe_history", ""),
+ "neutral_history": final_state.get("risk_debate_state", {}).get("neutral_history", ""),
+ "judge_decision": final_state.get("risk_debate_state", {}).get("judge_decision", ""),
+ },
+ "error": None,
+ }
+ except Exception as e:
+ return {
+ "symbol": symbol,
+ "company_name": NIFTY_50_STOCKS.get(symbol, symbol),
+ "decision": None,
+ "error": str(e),
+ }
+
+
+def predict_all_nifty50(
+ trade_date: str,
+ config: Optional[Dict[str, Any]] = None,
+ stock_subset: Optional[List[str]] = None,
+ on_progress: Optional[callable] = None
+) -> Dict[str, Dict[str, Any]]:
+ """
+ Run predictions for all 50 Nifty stocks (or a subset).
+
+ Args:
+ trade_date: Date for the predictions (YYYY-MM-DD format)
+ config: Optional configuration dictionary. If None, uses Claude config
+ stock_subset: Optional list of stock symbols to analyze. If None, analyzes all 50
+ on_progress: Optional callback function(current_index, total, symbol, result)
+
+ Returns:
+ Dictionary mapping stock symbols to their prediction results
+ """
+ if config is None:
+ config = create_claude_config()
+
+ # Verify Claude CLI is available for Max subscription
+ verify_claude_cli()
+
+ # Initialize the graph
+ graph = TradingAgentsGraph(
+ selected_analysts=["market", "social", "news", "fundamentals"],
+ debug=False,
+ config=config
+ )
+
+ # Get list of stocks to analyze
+ stocks = stock_subset if stock_subset else get_nifty_50_list()
+ total = len(stocks)
+
+ predictions = {}
+
+ for i, symbol in enumerate(stocks, 1):
+ result = predict_stock(graph, symbol, trade_date)
+ predictions[symbol] = result
+
+ if on_progress:
+ on_progress(i, total, symbol, result)
+
+ return predictions
+
+
+def format_predictions_for_prompt(predictions: Dict[str, Dict[str, Any]]) -> str:
+ """
+ Format all predictions into a comprehensive prompt for Claude.
+
+ Args:
+ predictions: Dictionary of prediction results
+
+ Returns:
+ Formatted string containing all predictions
+ """
+ formatted_parts = []
+
+ for symbol, pred in predictions.items():
+ if pred.get("error"):
+ formatted_parts.append(f"""
+=== {symbol} ({pred.get('company_name', symbol)}) ===
+ERROR: {pred['error']}
+""")
+ continue
+
+ formatted_parts.append(f"""
+=== {symbol} ({pred.get('company_name', symbol)}) ===
+
+DECISION: {pred.get('decision', 'N/A')}
+
+MARKET ANALYSIS:
+{pred.get('market_report', 'N/A')[:1000]}
+
+FUNDAMENTALS:
+{pred.get('fundamentals_report', 'N/A')[:1000]}
+
+NEWS & SENTIMENT:
+{pred.get('news_report', 'N/A')[:500]}
+{pred.get('sentiment_report', 'N/A')[:500]}
+
+INVESTMENT PLAN:
+{pred.get('investment_plan', 'N/A')[:500]}
+
+FINAL TRADE DECISION:
+{pred.get('final_trade_decision', 'N/A')[:500]}
+
+BULL/BEAR DEBATE SUMMARY:
+Bull: {pred.get('investment_debate', {}).get('bull_history', 'N/A')[:300]}
+Bear: {pred.get('investment_debate', {}).get('bear_history', 'N/A')[:300]}
+Judge: {pred.get('investment_debate', {}).get('judge_decision', 'N/A')[:300]}
+
+RISK ASSESSMENT:
+{pred.get('risk_debate', {}).get('judge_decision', 'N/A')[:300]}
+
+---
+""")
+
+ return "\n".join(formatted_parts)
+
+
+def parse_ranking_response(response_text: str) -> Dict[str, Any]:
+ """
+ Parse the ranking response from Claude.
+
+ Args:
+ response_text: Raw response text from Claude
+
+ Returns:
+ Dictionary containing parsed ranking results
+ """
+ return {
+ "raw_response": response_text,
+ "parsed": True,
+ }
+
+
+def rank_stocks_for_growth(
+ predictions: Dict[str, Dict[str, Any]],
+) -> Dict[str, Any]:
+ """
+ Use Claude Opus 4.5 to rank stocks by short-term growth potential.
+
+ Args:
+ predictions: Dictionary of prediction results for all stocks
+
+ Returns:
+ Dictionary containing ranking results with top picks and stocks to avoid
+ """
+ # Initialize Claude Opus via Max subscription
+ llm = ClaudeMaxLLM(model="opus")
+
+ # Filter out stocks with errors
+ valid_predictions = {
+ k: v for k, v in predictions.items()
+ if not v.get("error")
+ }
+
+ if not valid_predictions:
+ return {
+ "error": "No valid predictions to rank",
+ "top_picks": [],
+ "stocks_to_avoid": [],
+ }
+
+ # Format predictions for prompt
+ formatted = format_predictions_for_prompt(valid_predictions)
+
+ prompt = f"""You are an expert stock analyst specializing in the Indian equity market.
+Analyze the following predictions for Nifty 50 stocks and select the TOP 3 stocks with
+the highest short-term growth potential (1-2 weeks timeframe).
+
+For each stock, consider:
+1. BUY/SELL/HOLD decision and the confidence level
+2. Technical indicators and price momentum
+3. Fundamental strength (earnings, revenue, valuations)
+4. News sentiment and potential catalysts
+5. Risk factors and volatility
+
+STOCK PREDICTIONS:
+{formatted}
+
+Based on your comprehensive analysis, provide your recommendations in the following format:
+
+## TOP 3 PICKS FOR SHORT-TERM GROWTH
+
+### 1. TOP PICK: [SYMBOL]
+**Company:** [Company Name]
+**Recommendation:** [BUY/STRONG BUY]
+**Target Upside:** [X%]
+**Reasoning:** [2-3 sentences explaining why this is the top pick, citing specific data points]
+**Key Catalysts:** [List 2-3 near-term catalysts]
+**Risk Level:** [Low/Medium/High]
+
+### 2. SECOND PICK: [SYMBOL]
+**Company:** [Company Name]
+**Recommendation:** [BUY/STRONG BUY]
+**Target Upside:** [X%]
+**Reasoning:** [2-3 sentences]
+**Key Catalysts:** [List 2-3 near-term catalysts]
+**Risk Level:** [Low/Medium/High]
+
+### 3. THIRD PICK: [SYMBOL]
+**Company:** [Company Name]
+**Recommendation:** [BUY/STRONG BUY]
+**Target Upside:** [X%]
+**Reasoning:** [2-3 sentences]
+**Key Catalysts:** [List 2-3 near-term catalysts]
+**Risk Level:** [Low/Medium/High]
+
+## STOCKS TO AVOID
+
+List any stocks that show concerning signals and should be avoided:
+- [SYMBOL]: [Brief reason - e.g., "Bearish technical setup, negative news flow"]
+- [SYMBOL]: [Brief reason]
+
+## MARKET CONTEXT
+
+Provide a brief (2-3 sentences) overview of the current market conditions affecting these recommendations.
+
+## DISCLAIMER
+
+Include a brief investment disclaimer.
+"""
+
+ # Use Claude Opus 4.5's large context window
+ response = llm.invoke(prompt)
+
+ return {
+ "ranking_analysis": response.content,
+ "total_stocks_analyzed": len(valid_predictions),
+ "stocks_with_errors": len(predictions) - len(valid_predictions),
+ "timestamp": datetime.now().isoformat(),
+ }
+
+
+def run_recommendation(
+ trade_date: str,
+ stock_subset: Optional[List[str]] = None,
+ save_results: bool = True,
+ results_dir: Optional[str] = None,
+ verbose: bool = True
+) -> Tuple[Dict[str, Dict[str, Any]], Dict[str, Any]]:
+ """
+ Main entry point for running the Nifty 50 recommendation system.
+
+ Args:
+ trade_date: Date for the predictions (YYYY-MM-DD format)
+ stock_subset: Optional list of stock symbols to analyze. If None, analyzes all 50
+ save_results: Whether to save results to disk
+ results_dir: Directory to save results. If None, uses default
+ verbose: Whether to print progress updates
+
+ Returns:
+ Tuple of (predictions dict, ranking results dict)
+ """
+ def progress_callback(current, total, symbol, result):
+ if verbose:
+ status = "✓" if not result.get("error") else "✗"
+ decision = result.get("decision", "ERROR") if not result.get("error") else "ERROR"
+ print(f"[{current}/{total}] {symbol}: {status} {decision}")
+
+ if verbose:
+ print(f"\n{'='*60}")
+ print(f"NIFTY 50 STOCK RECOMMENDATION SYSTEM")
+ print(f"{'='*60}")
+ print(f"Date: {trade_date}")
+ stocks = stock_subset if stock_subset else get_nifty_50_list()
+ print(f"Analyzing {len(stocks)} stocks...")
+ print(f"{'='*60}\n")
+
+ # Run predictions for all stocks
+ predictions = predict_all_nifty50(
+ trade_date=trade_date,
+ stock_subset=stock_subset,
+ on_progress=progress_callback
+ )
+
+ if verbose:
+ successful = sum(1 for p in predictions.values() if not p.get("error"))
+ print(f"\n{'='*60}")
+ print(f"Predictions Complete: {successful}/{len(predictions)} successful")
+ print(f"{'='*60}\n")
+ print("Ranking stocks with Claude Opus 4.5...")
+
+ # Rank stocks using Claude Opus 4.5
+ ranking = rank_stocks_for_growth(predictions)
+
+ if verbose:
+ print(f"\n{'='*60}")
+ print("RECOMMENDATION RESULTS")
+ print(f"{'='*60}\n")
+ print(ranking.get("ranking_analysis", "No ranking available"))
+
+ # Save results if requested
+ if save_results:
+ if results_dir is None:
+ results_dir = Path(DEFAULT_CONFIG["results_dir"]) / "nifty50_recommendations"
+ else:
+ results_dir = Path(results_dir)
+
+ results_dir.mkdir(parents=True, exist_ok=True)
+
+ # Save predictions
+ predictions_file = results_dir / f"predictions_{trade_date}.json"
+ with open(predictions_file, "w") as f:
+ # Convert to serializable format
+ serializable_predictions = {}
+ for symbol, pred in predictions.items():
+ serializable_predictions[symbol] = {
+ k: str(v) if not isinstance(v, (str, dict, list, type(None))) else v
+ for k, v in pred.items()
+ }
+ json.dump(serializable_predictions, f, indent=2)
+
+ # Save ranking
+ ranking_file = results_dir / f"ranking_{trade_date}.json"
+ with open(ranking_file, "w") as f:
+ json.dump(ranking, f, indent=2)
+
+ # Save readable report
+ report_file = results_dir / f"report_{trade_date}.md"
+ with open(report_file, "w") as f:
+ f.write(f"# Nifty 50 Stock Recommendation Report\n\n")
+ f.write(f"**Date:** {trade_date}\n\n")
+ f.write(f"**Stocks Analyzed:** {ranking.get('total_stocks_analyzed', 0)}\n\n")
+ f.write(f"**Generated:** {ranking.get('timestamp', 'N/A')}\n\n")
+ f.write("---\n\n")
+ f.write(ranking.get("ranking_analysis", "No ranking available"))
+
+ if verbose:
+ print(f"\nResults saved to: {results_dir}")
+
+ return predictions, ranking
diff --git a/tradingagents/nifty50_simple_recommender.py b/tradingagents/nifty50_simple_recommender.py
new file mode 100644
index 00000000..ba1fb28e
--- /dev/null
+++ b/tradingagents/nifty50_simple_recommender.py
@@ -0,0 +1,364 @@
+"""
+Simplified Nifty 50 Stock Recommendation System.
+
+This module uses Claude Max subscription (via CLI) to analyze pre-fetched stock data
+and provide recommendations. It bypasses the complex agent framework to work with
+Claude Max subscription without API keys.
+"""
+
+import os
+# Disable CUDA to avoid library issues with embeddings
+os.environ["CUDA_VISIBLE_DEVICES"] = ""
+# Remove API key to force Claude Max subscription auth
+os.environ.pop("ANTHROPIC_API_KEY", None)
+
+import json
+from pathlib import Path
+from typing import Dict, Any, Optional, List, Tuple
+from datetime import datetime, timedelta
+
+from tradingagents.dataflows.markets import NIFTY_50_STOCKS, get_nifty_50_list
+from tradingagents.claude_max_llm import ClaudeMaxLLM
+
+# Import data fetching tools
+from tradingagents.agents.utils.agent_utils import (
+ get_stock_data,
+ get_indicators,
+ get_fundamentals,
+ get_news,
+)
+
+
+def fetch_stock_data(symbol: str, trade_date: str) -> Dict[str, str]:
+ """
+ Fetch all relevant data for a stock.
+
+ Args:
+ symbol: Stock symbol (e.g., 'TCS', 'RELIANCE')
+ trade_date: Date for analysis (YYYY-MM-DD)
+
+ Returns:
+ Dictionary with stock data, indicators, fundamentals, and news
+ """
+ # Calculate date range (10 days before trade date)
+ end_date = trade_date
+ start_date = (datetime.strptime(trade_date, "%Y-%m-%d") - timedelta(days=30)).strftime("%Y-%m-%d")
+
+ data = {}
+
+ # Fetch stock price data
+ try:
+ data["price_data"] = get_stock_data.invoke({
+ "symbol": symbol,
+ "start_date": start_date,
+ "end_date": end_date
+ })
+ except Exception as e:
+ data["price_data"] = f"Error fetching price data: {e}"
+
+ # Fetch technical indicators
+ try:
+ data["indicators"] = get_indicators.invoke({
+ "symbol": symbol,
+ "start_date": start_date,
+ "end_date": end_date
+ })
+ except Exception as e:
+ data["indicators"] = f"Error fetching indicators: {e}"
+
+ # Fetch fundamentals
+ try:
+ data["fundamentals"] = get_fundamentals.invoke({
+ "symbol": symbol
+ })
+ except Exception as e:
+ data["fundamentals"] = f"Error fetching fundamentals: {e}"
+
+ # Fetch news
+ try:
+ company_name = NIFTY_50_STOCKS.get(symbol, symbol)
+ data["news"] = get_news.invoke({
+ "symbol": symbol,
+ "company_name": company_name
+ })
+ except Exception as e:
+ data["news"] = f"Error fetching news: {e}"
+
+ return data
+
+
+def analyze_stock(symbol: str, data: Dict[str, str], llm: ClaudeMaxLLM) -> Dict[str, Any]:
+ """
+ Use Claude to analyze a stock based on pre-fetched data.
+
+ Args:
+ symbol: Stock symbol
+ data: Pre-fetched stock data
+ llm: ClaudeMaxLLM instance
+
+ Returns:
+ Analysis result with decision and reasoning
+ """
+ company_name = NIFTY_50_STOCKS.get(symbol, symbol)
+
+ prompt = f"""You are an expert stock analyst. Analyze the following data for {symbol} ({company_name}) and provide:
+1. A trading decision: BUY, SELL, or HOLD
+2. Confidence level: High, Medium, or Low
+3. Key reasoning (2-3 sentences)
+4. Risk assessment: High, Medium, or Low
+
+## Price Data (Last 30 days)
+{data.get('price_data', 'Not available')[:2000]}
+
+## Technical Indicators
+{data.get('indicators', 'Not available')[:2000]}
+
+## Fundamentals
+{data.get('fundamentals', 'Not available')[:2000]}
+
+## Recent News
+{data.get('news', 'Not available')[:1500]}
+
+Provide your analysis in this exact format:
+DECISION: [BUY/SELL/HOLD]
+CONFIDENCE: [High/Medium/Low]
+REASONING: [Your 2-3 sentence reasoning]
+RISK: [High/Medium/Low]
+TARGET: [Expected price movement in next 1-2 weeks, e.g., "+5%" or "-3%"]
+"""
+
+ try:
+ response = llm.invoke(prompt)
+ analysis_text = response.content
+
+ # Parse the response
+ result = {
+ "symbol": symbol,
+ "company_name": company_name,
+ "raw_analysis": analysis_text,
+ "error": None
+ }
+
+ # Extract structured data (handle markdown formatting like **DECISION:**)
+ import re
+ text_upper = analysis_text.upper()
+
+ # Look for DECISION
+ decision_match = re.search(r'\*?\*?DECISION:?\*?\*?\s*([A-Z]+)', text_upper)
+ if decision_match:
+ result["decision"] = decision_match.group(1).strip()
+
+ # Look for CONFIDENCE
+ confidence_match = re.search(r'\*?\*?CONFIDENCE:?\*?\*?\s*([A-Z]+)', text_upper)
+ if confidence_match:
+ result["confidence"] = confidence_match.group(1).strip()
+
+ # Look for RISK
+ risk_match = re.search(r'\*?\*?RISK:?\*?\*?\s*([A-Z]+)', text_upper)
+ if risk_match:
+ result["risk"] = risk_match.group(1).strip()
+
+ return result
+
+ except Exception as e:
+ return {
+ "symbol": symbol,
+ "company_name": company_name,
+ "decision": None,
+ "error": str(e)
+ }
+
+
+def analyze_all_stocks(
+ trade_date: str,
+ stock_subset: Optional[List[str]] = None,
+ on_progress: Optional[callable] = None
+) -> Dict[str, Dict[str, Any]]:
+ """
+ Analyze all Nifty 50 stocks (or a subset).
+
+ Args:
+ trade_date: Date for analysis
+ stock_subset: Optional list of symbols to analyze
+ on_progress: Optional callback(current, total, symbol, result)
+
+ Returns:
+ Dictionary of analysis results
+ """
+ # Initialize Claude LLM
+ llm = ClaudeMaxLLM(model="sonnet")
+
+ stocks = stock_subset if stock_subset else get_nifty_50_list()
+ total = len(stocks)
+ results = {}
+
+ for i, symbol in enumerate(stocks, 1):
+ # Fetch data
+ data = fetch_stock_data(symbol, trade_date)
+
+ # Analyze with Claude
+ analysis = analyze_stock(symbol, data, llm)
+ results[symbol] = analysis
+
+ if on_progress:
+ on_progress(i, total, symbol, analysis)
+
+ return results
+
+
+def rank_stocks(results: Dict[str, Dict[str, Any]]) -> Dict[str, Any]:
+ """
+ Rank stocks by growth potential using Claude Opus.
+
+ Args:
+ results: Analysis results for all stocks
+
+ Returns:
+ Ranking with top picks
+ """
+ llm = ClaudeMaxLLM(model="opus")
+
+ # Build summary of all analyses
+ summaries = []
+ for symbol, analysis in results.items():
+ if analysis.get("error"):
+ continue
+ summaries.append(f"""
+{symbol} ({analysis.get('company_name', symbol)}):
+- Decision: {analysis.get('decision', 'N/A')}
+- Confidence: {analysis.get('confidence', 'N/A')}
+- Risk: {analysis.get('risk', 'N/A')}
+- Analysis: {analysis.get('raw_analysis', 'N/A')[:300]}
+""")
+
+ if not summaries:
+ return {"error": "No valid analyses to rank", "ranking": None}
+
+ prompt = f"""You are an expert stock analyst. Based on the following analyses of Nifty 50 stocks,
+identify the TOP 3 stocks with highest short-term growth potential (1-2 weeks).
+
+## Stock Analyses
+{''.join(summaries)}
+
+Provide your ranking in this format:
+
+## TOP 3 PICKS
+
+### 1. [SYMBOL] - TOP PICK
+**Decision:** [BUY/STRONG BUY]
+**Reason:** [2-3 sentences explaining why this is the top pick]
+**Risk Level:** [Low/Medium/High]
+
+### 2. [SYMBOL] - SECOND PICK
+**Decision:** [BUY]
+**Reason:** [2-3 sentences]
+**Risk Level:** [Low/Medium/High]
+
+### 3. [SYMBOL] - THIRD PICK
+**Decision:** [BUY]
+**Reason:** [2-3 sentences]
+**Risk Level:** [Low/Medium/High]
+
+## STOCKS TO AVOID
+List 2-3 stocks that should be avoided with brief reasons.
+"""
+
+ try:
+ response = llm.invoke(prompt)
+ return {
+ "ranking": response.content,
+ "stocks_analyzed": len(summaries),
+ "timestamp": datetime.now().isoformat()
+ }
+ except Exception as e:
+ return {"error": str(e), "ranking": None}
+
+
+def run_recommendation(
+ trade_date: str,
+ stock_subset: Optional[List[str]] = None,
+ save_results: bool = True,
+ results_dir: Optional[str] = None,
+ verbose: bool = True
+) -> Tuple[Dict[str, Dict[str, Any]], Dict[str, Any]]:
+ """
+ Main entry point for the recommendation system.
+
+ Args:
+ trade_date: Date for analysis
+ stock_subset: Optional list of symbols
+ save_results: Whether to save results to disk
+ results_dir: Directory for results
+ verbose: Print progress
+
+ Returns:
+ Tuple of (analysis results, ranking)
+ """
+ def progress_callback(current, total, symbol, result):
+ if verbose:
+ status = "✓" if not result.get("error") else "✗"
+ decision = result.get("decision", "ERROR") if not result.get("error") else "ERROR"
+ print(f"[{current}/{total}] {symbol}: {status} {decision}")
+
+ if verbose:
+ print(f"\n{'='*60}")
+ print("NIFTY 50 STOCK RECOMMENDATION SYSTEM (Simple)")
+ print(f"{'='*60}")
+ print(f"Date: {trade_date}")
+ stocks = stock_subset if stock_subset else get_nifty_50_list()
+ print(f"Analyzing {len(stocks)} stocks...")
+ print(f"Using Claude Max subscription via CLI")
+ print(f"{'='*60}\n")
+
+ # Analyze all stocks
+ results = analyze_all_stocks(trade_date, stock_subset, progress_callback)
+
+ if verbose:
+ successful = sum(1 for r in results.values() if not r.get("error"))
+ print(f"\n{'='*60}")
+ print(f"Analysis Complete: {successful}/{len(results)} successful")
+ print(f"{'='*60}\n")
+ print("Ranking stocks with Claude Opus...")
+
+ # Rank stocks
+ ranking = rank_stocks(results)
+
+ if verbose:
+ print(f"\n{'='*60}")
+ print("RECOMMENDATION RESULTS")
+ print(f"{'='*60}\n")
+ if ranking.get("ranking"):
+ print(ranking["ranking"])
+ else:
+ print(f"Error: {ranking.get('error', 'Unknown error')}")
+
+ # Save results if requested
+ if save_results:
+ from tradingagents.default_config import DEFAULT_CONFIG
+ if results_dir is None:
+ results_dir = Path(DEFAULT_CONFIG["results_dir"]) / "nifty50_simple_recommendations"
+ else:
+ results_dir = Path(results_dir)
+
+ results_dir.mkdir(parents=True, exist_ok=True)
+
+ # Save analysis results
+ with open(results_dir / f"analysis_{trade_date}.json", "w") as f:
+ json.dump(results, f, indent=2, default=str)
+
+ # Save ranking
+ with open(results_dir / f"ranking_{trade_date}.json", "w") as f:
+ json.dump(ranking, f, indent=2, default=str)
+
+ # Save readable report
+ with open(results_dir / f"report_{trade_date}.md", "w") as f:
+ f.write(f"# Nifty 50 Stock Recommendation Report\n\n")
+ f.write(f"**Date:** {trade_date}\n\n")
+ f.write(f"**Stocks Analyzed:** {ranking.get('stocks_analyzed', 0)}\n\n")
+ f.write("---\n\n")
+ f.write(ranking.get("ranking", "No ranking available"))
+
+ if verbose:
+ print(f"\nResults saved to: {results_dir}")
+
+ return results, ranking