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 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + 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 +
+
{buy}
+
{buyPercent}%
+
+ +
+
+
+ Hold +
+
{hold}
+
{holdPercent}%
+
+ +
+
+
+ Sell +
+
{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 */} +
+
+
+ +
+

Nifty50 AI

+
+

+ AI-powered stock recommendations for Nifty 50 stocks. Using advanced machine learning + to analyze market trends, technical indicators, and news sentiment. +

+
+ + {/* Quick Links */} +
+

Quick Links

+ +
+ + {/* Legal */} +
+

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 ( +
+
+
+ {/* Logo */} + +
+ +
+
+

Nifty50 AI

+

Stock Recommendations

+
+ + + {/* Desktop Navigation */} + + + {/* Mobile Menu Button */} + +
+ + {/* Mobile Navigation */} + {mobileMenuOpen && ( + + )} +
+
+ ); +} 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 to Avoid

+
+ +
+ {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 }) => ( + + ))} +
+
+
+ +
+ {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 ( + + ); + })} +
+
+ + {/* 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 */} +
+ + +
+
+ +

+ 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