Add Nifty50 AI Trading Dashboard frontend and Indian market support

- Add React + Vite + Tailwind CSS frontend for Nifty50 recommendations
- Add FastAPI backend for serving stock recommendations
- Add Indian market data sources (jugaad_data, markets API)
- Add Nifty50 stock recommender modules
- Update dataflows for Indian market support
- Fix various utility and configuration updates
This commit is contained in:
hemangjoshi37a 2026-01-31 19:41:01 +11:00
parent 7902d249ca
commit e43acb8247
54 changed files with 4962 additions and 99 deletions

9
.gitignore vendored
View File

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

View File

@ -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'])}",

View File

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

24
frontend/.gitignore vendored Normal file
View File

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

73
frontend/README.md Normal file
View File

@ -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...
},
},
])
```

View File

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

Binary file not shown.

View File

@ -0,0 +1,3 @@
fastapi>=0.109.0
uvicorn>=0.27.0
pydantic>=2.0.0

View File

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

151
frontend/backend/server.py Normal file
View File

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

23
frontend/eslint.config.js Normal file
View File

@ -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,
},
},
])

86
frontend/index.html Normal file
View File

@ -0,0 +1,86 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<!-- Primary Meta Tags -->
<title>Nifty50 AI - Daily Stock Recommendations for Indian Markets</title>
<meta name="title" content="Nifty50 AI - Daily Stock Recommendations for Indian Markets" />
<meta name="description" content="AI-powered daily stock recommendations for all Nifty 50 stocks. Get actionable buy, sell, and hold signals based on technical analysis, fundamentals, and news sentiment." />
<meta name="keywords" content="Nifty 50, stock recommendations, AI stock analysis, Indian stock market, NSE, BSE, trading signals, buy sell hold, stock market India" />
<meta name="author" content="Nifty50 AI" />
<meta name="robots" content="index, follow" />
<!-- Favicon -->
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png" />
<!-- Open Graph / Facebook -->
<meta property="og:type" content="website" />
<meta property="og:url" content="https://nifty50ai.com/" />
<meta property="og:title" content="Nifty50 AI - Daily Stock Recommendations for Indian Markets" />
<meta property="og:description" content="AI-powered daily stock recommendations for all Nifty 50 stocks. Get actionable buy, sell, and hold signals." />
<meta property="og:image" content="/og-image.png" />
<meta property="og:locale" content="en_IN" />
<meta property="og:site_name" content="Nifty50 AI" />
<!-- Twitter -->
<meta property="twitter:card" content="summary_large_image" />
<meta property="twitter:url" content="https://nifty50ai.com/" />
<meta property="twitter:title" content="Nifty50 AI - Daily Stock Recommendations for Indian Markets" />
<meta property="twitter:description" content="AI-powered daily stock recommendations for all Nifty 50 stocks. Get actionable buy, sell, and hold signals." />
<meta property="twitter:image" content="/og-image.png" />
<!-- Theme Color -->
<meta name="theme-color" content="#0284c7" />
<meta name="msapplication-TileColor" content="#0284c7" />
<!-- Canonical URL -->
<link rel="canonical" href="https://nifty50ai.com/" />
<!-- Google Fonts -->
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&family=Lexend:wght@400;500;600;700&display=swap" rel="stylesheet">
<!-- Structured Data (JSON-LD) -->
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "WebSite",
"name": "Nifty50 AI",
"description": "AI-powered daily stock recommendations for all Nifty 50 stocks",
"url": "https://nifty50ai.com/",
"potentialAction": {
"@type": "SearchAction",
"target": "https://nifty50ai.com/stock/{search_term_string}",
"query-input": "required name=search_term_string"
}
}
</script>
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "Organization",
"name": "Nifty50 AI",
"url": "https://nifty50ai.com/",
"logo": "https://nifty50ai.com/logo.png",
"description": "AI-powered stock analysis and recommendations for Indian markets"
}
</script>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
<!-- Noscript fallback -->
<noscript>
<div style="padding: 20px; text-align: center; font-family: system-ui, sans-serif;">
<h1>Nifty50 AI - Stock Recommendations</h1>
<p>Please enable JavaScript to view this website.</p>
</div>
</noscript>
</body>
</html>

39
frontend/package.json Normal file
View File

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

View File

@ -0,0 +1,6 @@
export default {
plugins: {
'@tailwindcss/postcss': {},
autoprefixer: {},
},
}

View File

@ -0,0 +1,11 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
<defs>
<linearGradient id="bg" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" style="stop-color:#0ea5e9"/>
<stop offset="100%" style="stop-color:#0369a1"/>
</linearGradient>
</defs>
<rect width="64" height="64" rx="14" fill="url(#bg)"/>
<path d="M16 44 L26 28 L36 36 L48 20" stroke="white" stroke-width="4" stroke-linecap="round" stroke-linejoin="round" fill="none"/>
<circle cx="48" cy="20" r="4" fill="#22c55e"/>
</svg>

After

Width:  |  Height:  |  Size: 521 B

1
frontend/public/vite.svg Normal file
View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

42
frontend/src/App.css Normal file
View File

@ -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;
}

26
frontend/src/App.tsx Normal file
View File

@ -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 (
<div className="min-h-screen flex flex-col">
<Header />
<main className="flex-1 max-w-7xl mx-auto w-full px-4 sm:px-6 lg:px-8 py-8">
<Routes>
<Route path="/" element={<Dashboard />} />
<Route path="/history" element={<History />} />
<Route path="/stocks" element={<Stocks />} />
<Route path="/stock/:symbol" element={<StockDetail />} />
</Routes>
</main>
<Footer />
</div>
);
}
export default App;

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="35.93" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 228"><path fill="#00D8FF" d="M210.483 73.824a171.49 171.49 0 0 0-8.24-2.597c.465-1.9.893-3.777 1.273-5.621c6.238-30.281 2.16-54.676-11.769-62.708c-13.355-7.7-35.196.329-57.254 19.526a171.23 171.23 0 0 0-6.375 5.848a155.866 155.866 0 0 0-4.241-3.917C100.759 3.829 77.587-4.822 63.673 3.233C50.33 10.957 46.379 33.89 51.995 62.588a170.974 170.974 0 0 0 1.892 8.48c-3.28.932-6.445 1.924-9.474 2.98C17.309 83.498 0 98.307 0 113.668c0 15.865 18.582 31.778 46.812 41.427a145.52 145.52 0 0 0 6.921 2.165a167.467 167.467 0 0 0-2.01 9.138c-5.354 28.2-1.173 50.591 12.134 58.266c13.744 7.926 36.812-.22 59.273-19.855a145.567 145.567 0 0 0 5.342-4.923a168.064 168.064 0 0 0 6.92 6.314c21.758 18.722 43.246 26.282 56.54 18.586c13.731-7.949 18.194-32.003 12.4-61.268a145.016 145.016 0 0 0-1.535-6.842c1.62-.48 3.21-.974 4.76-1.488c29.348-9.723 48.443-25.443 48.443-41.52c0-15.417-17.868-30.326-45.517-39.844Zm-6.365 70.984c-1.4.463-2.836.91-4.3 1.345c-3.24-10.257-7.612-21.163-12.963-32.432c5.106-11 9.31-21.767 12.459-31.957c2.619.758 5.16 1.557 7.61 2.4c23.69 8.156 38.14 20.213 38.14 29.504c0 9.896-15.606 22.743-40.946 31.14Zm-10.514 20.834c2.562 12.94 2.927 24.64 1.23 33.787c-1.524 8.219-4.59 13.698-8.382 15.893c-8.067 4.67-25.32-1.4-43.927-17.412a156.726 156.726 0 0 1-6.437-5.87c7.214-7.889 14.423-17.06 21.459-27.246c12.376-1.098 24.068-2.894 34.671-5.345a134.17 134.17 0 0 1 1.386 6.193ZM87.276 214.515c-7.882 2.783-14.16 2.863-17.955.675c-8.075-4.657-11.432-22.636-6.853-46.752a156.923 156.923 0 0 1 1.869-8.499c10.486 2.32 22.093 3.988 34.498 4.994c7.084 9.967 14.501 19.128 21.976 27.15a134.668 134.668 0 0 1-4.877 4.492c-9.933 8.682-19.886 14.842-28.658 17.94ZM50.35 144.747c-12.483-4.267-22.792-9.812-29.858-15.863c-6.35-5.437-9.555-10.836-9.555-15.216c0-9.322 13.897-21.212 37.076-29.293c2.813-.98 5.757-1.905 8.812-2.773c3.204 10.42 7.406 21.315 12.477 32.332c-5.137 11.18-9.399 22.249-12.634 32.792a134.718 134.718 0 0 1-6.318-1.979Zm12.378-84.26c-4.811-24.587-1.616-43.134 6.425-47.789c8.564-4.958 27.502 2.111 47.463 19.835a144.318 144.318 0 0 1 3.841 3.545c-7.438 7.987-14.787 17.08-21.808 26.988c-12.04 1.116-23.565 2.908-34.161 5.309a160.342 160.342 0 0 1-1.76-7.887Zm110.427 27.268a347.8 347.8 0 0 0-7.785-12.803c8.168 1.033 15.994 2.404 23.343 4.08c-2.206 7.072-4.956 14.465-8.193 22.045a381.151 381.151 0 0 0-7.365-13.322Zm-45.032-43.861c5.044 5.465 10.096 11.566 15.065 18.186a322.04 322.04 0 0 0-30.257-.006c4.974-6.559 10.069-12.652 15.192-18.18ZM82.802 87.83a323.167 323.167 0 0 0-7.227 13.238c-3.184-7.553-5.909-14.98-8.134-22.152c7.304-1.634 15.093-2.97 23.209-3.984a321.524 321.524 0 0 0-7.848 12.897Zm8.081 65.352c-8.385-.936-16.291-2.203-23.593-3.793c2.26-7.3 5.045-14.885 8.298-22.6a321.187 321.187 0 0 0 7.257 13.246c2.594 4.48 5.28 8.868 8.038 13.147Zm37.542 31.03c-5.184-5.592-10.354-11.779-15.403-18.433c4.902.192 9.899.29 14.978.29c5.218 0 10.376-.117 15.453-.343c-4.985 6.774-10.018 12.97-15.028 18.486Zm52.198-57.817c3.422 7.8 6.306 15.345 8.596 22.52c-7.422 1.694-15.436 3.058-23.88 4.071a382.417 382.417 0 0 0 7.859-13.026a347.403 347.403 0 0 0 7.425-13.565Zm-16.898 8.101a358.557 358.557 0 0 1-12.281 19.815a329.4 329.4 0 0 1-23.444.823c-7.967 0-15.716-.248-23.178-.732a310.202 310.202 0 0 1-12.513-19.846h.001a307.41 307.41 0 0 1-10.923-20.627a310.278 310.278 0 0 1 10.89-20.637l-.001.001a307.318 307.318 0 0 1 12.413-19.761c7.613-.576 15.42-.876 23.31-.876H128c7.926 0 15.743.303 23.354.883a329.357 329.357 0 0 1 12.335 19.695a358.489 358.489 0 0 1 11.036 20.54a329.472 329.472 0 0 1-11 20.722Zm22.56-122.124c8.572 4.944 11.906 24.881 6.52 51.026c-.344 1.668-.73 3.367-1.15 5.09c-10.622-2.452-22.155-4.275-34.23-5.408c-7.034-10.017-14.323-19.124-21.64-27.008a160.789 160.789 0 0 1 5.888-5.4c18.9-16.447 36.564-22.941 44.612-18.3ZM128 90.808c12.625 0 22.86 10.235 22.86 22.86s-10.235 22.86-22.86 22.86s-22.86-10.235-22.86-22.86s10.235-22.86 22.86-22.86Z"></path></svg>

After

Width:  |  Height:  |  Size: 4.0 KiB

View File

@ -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 (
<div style={{ width: '100%', height: '256px' }}>
<ResponsiveContainer width="100%" height={256}>
<PieChart>
<Pie
data={data}
cx="50%"
cy="50%"
innerRadius={50}
outerRadius={80}
paddingAngle={4}
dataKey="value"
label={({ name, percent }) => `${name} ${((percent ?? 0) * 100).toFixed(0)}%`}
labelLine={false}
>
{data.map((entry, index) => (
<Cell key={`cell-${index}`} fill={entry.color} />
))}
</Pie>
<Tooltip
contentStyle={{
backgroundColor: 'white',
border: '1px solid #e5e7eb',
borderRadius: '8px',
boxShadow: '0 4px 6px -1px rgba(0, 0, 0, 0.1)',
}}
formatter={(value) => [`${value} stocks`, '']}
/>
<Legend
verticalAlign="bottom"
height={36}
formatter={(value) => <span className="text-sm text-gray-600">{value}</span>}
/>
</PieChart>
</ResponsiveContainer>
</div>
);
}
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 (
<div className="h-72">
<ResponsiveContainer width="100%" height="100%">
<BarChart data={formattedData} margin={{ top: 20, right: 30, left: 20, bottom: 5 }}>
<CartesianGrid strokeDasharray="3 3" stroke="#f0f0f0" />
<XAxis
dataKey="date"
tick={{ fontSize: 12, fill: '#6b7280' }}
tickLine={{ stroke: '#e5e7eb' }}
/>
<YAxis
tick={{ fontSize: 12, fill: '#6b7280' }}
tickLine={{ stroke: '#e5e7eb' }}
/>
<Tooltip
contentStyle={{
backgroundColor: 'white',
border: '1px solid #e5e7eb',
borderRadius: '8px',
boxShadow: '0 4px 6px -1px rgba(0, 0, 0, 0.1)',
}}
/>
<Legend
verticalAlign="top"
height={36}
formatter={(value) => <span className="text-sm text-gray-600 capitalize">{value}</span>}
/>
<Bar dataKey="buy" stackId="a" fill={COLORS.buy} radius={[4, 4, 0, 0]} name="Buy" />
<Bar dataKey="hold" stackId="a" fill={COLORS.hold} radius={[0, 0, 0, 0]} name="Hold" />
<Bar dataKey="sell" stackId="a" fill={COLORS.sell} radius={[0, 0, 4, 4]} name="Sell" />
</BarChart>
</ResponsiveContainer>
</div>
);
}
interface StockHistoryEntry {
date: string;
decision: string;
}
interface StockHistoryChartProps {
history: StockHistoryEntry[];
symbol: string;
}
export function StockHistoryTimeline({ history, symbol }: StockHistoryChartProps) {
if (history.length === 0) {
return (
<div className="text-center py-8 text-gray-500">
No historical data available for {symbol}
</div>
);
}
return (
<div className="space-y-3">
{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 (
<div key={idx} className="flex items-center gap-4">
<div className="w-24 text-sm text-gray-500">
{new Date(entry.date).toLocaleDateString('en-IN', { month: 'short', day: 'numeric' })}
</div>
<div className={`w-3 h-3 rounded-full ${bgColor}`} />
<div className={`text-sm font-medium ${textColor}`}>
{entry.decision}
</div>
</div>
);
})}
</div>
);
}
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 (
<div className="space-y-4">
<div className="flex h-4 rounded-full overflow-hidden bg-gray-100">
<div
className="bg-green-500 transition-all duration-500"
style={{ width: `${(buy / total) * 100}%` }}
title={`Buy: ${buy} (${buyPercent}%)`}
/>
<div
className="bg-amber-500 transition-all duration-500"
style={{ width: `${(hold / total) * 100}%` }}
title={`Hold: ${hold} (${holdPercent}%)`}
/>
<div
className="bg-red-500 transition-all duration-500"
style={{ width: `${(sell / total) * 100}%` }}
title={`Sell: ${sell} (${sellPercent}%)`}
/>
</div>
<div className="grid grid-cols-3 gap-4 text-center">
<div>
<div className="flex items-center justify-center gap-2 mb-1">
<div className="w-3 h-3 rounded-full bg-green-500" />
<span className="text-sm font-medium text-gray-700">Buy</span>
</div>
<div className="text-2xl font-bold text-green-600">{buy}</div>
<div className="text-xs text-gray-500">{buyPercent}%</div>
</div>
<div>
<div className="flex items-center justify-center gap-2 mb-1">
<div className="w-3 h-3 rounded-full bg-amber-500" />
<span className="text-sm font-medium text-gray-700">Hold</span>
</div>
<div className="text-2xl font-bold text-amber-600">{hold}</div>
<div className="text-xs text-gray-500">{holdPercent}%</div>
</div>
<div>
<div className="flex items-center justify-center gap-2 mb-1">
<div className="w-3 h-3 rounded-full bg-red-500" />
<span className="text-sm font-medium text-gray-700">Sell</span>
</div>
<div className="text-2xl font-bold text-red-600">{sell}</div>
<div className="text-xs text-gray-500">{sellPercent}%</div>
</div>
</div>
</div>
);
}

View File

@ -0,0 +1,69 @@
import { TrendingUp, Github, Twitter } from 'lucide-react';
export default function Footer() {
return (
<footer className="bg-white border-t border-gray-200 mt-auto">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-12">
<div className="grid grid-cols-1 md:grid-cols-4 gap-8">
{/* Brand */}
<div className="col-span-1 md:col-span-2">
<div className="flex items-center gap-2 mb-4">
<div className="w-10 h-10 bg-gradient-to-br from-nifty-500 to-nifty-700 rounded-xl flex items-center justify-center">
<TrendingUp className="w-6 h-6 text-white" />
</div>
<h2 className="text-xl font-display font-bold gradient-text">Nifty50 AI</h2>
</div>
<p className="text-gray-600 text-sm max-w-md">
AI-powered stock recommendations for Nifty 50 stocks. Using advanced machine learning
to analyze market trends, technical indicators, and news sentiment.
</p>
</div>
{/* Quick Links */}
<div>
<h3 className="font-semibold text-gray-900 mb-4">Quick Links</h3>
<ul className="space-y-2">
<li><a href="/" className="text-gray-600 hover:text-nifty-600 text-sm">Dashboard</a></li>
<li><a href="/history" className="text-gray-600 hover:text-nifty-600 text-sm">History</a></li>
<li><a href="/stocks" className="text-gray-600 hover:text-nifty-600 text-sm">All Stocks</a></li>
</ul>
</div>
{/* Legal */}
<div>
<h3 className="font-semibold text-gray-900 mb-4">Legal</h3>
<ul className="space-y-2">
<li><a href="#" className="text-gray-600 hover:text-nifty-600 text-sm">Disclaimer</a></li>
<li><a href="#" className="text-gray-600 hover:text-nifty-600 text-sm">Privacy Policy</a></li>
<li><a href="#" className="text-gray-600 hover:text-nifty-600 text-sm">Terms of Use</a></li>
</ul>
</div>
</div>
<div className="border-t border-gray-200 mt-8 pt-8 flex flex-col sm:flex-row justify-between items-center gap-4">
<p className="text-gray-500 text-sm">
© {new Date().getFullYear()} Nifty50 AI. All rights reserved.
</p>
<div className="flex items-center gap-4">
<a href="#" className="text-gray-400 hover:text-gray-600">
<Github className="w-5 h-5" />
</a>
<a href="#" className="text-gray-400 hover:text-gray-600">
<Twitter className="w-5 h-5" />
</a>
</div>
</div>
{/* Disclaimer */}
<div className="mt-8 p-4 bg-gray-50 rounded-lg">
<p className="text-xs text-gray-500 text-center">
<strong>Disclaimer:</strong> 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.
</p>
</div>
</div>
</footer>
);
}

View File

@ -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 (
<header className="bg-white border-b border-gray-200 sticky top-0 z-50">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div className="flex justify-between items-center h-16">
{/* Logo */}
<Link to="/" className="flex items-center gap-2">
<div className="w-10 h-10 bg-gradient-to-br from-nifty-500 to-nifty-700 rounded-xl flex items-center justify-center">
<TrendingUp className="w-6 h-6 text-white" />
</div>
<div className="hidden sm:block">
<h1 className="text-xl font-display font-bold gradient-text">Nifty50 AI</h1>
<p className="text-xs text-gray-500 -mt-1">Stock Recommendations</p>
</div>
</Link>
{/* Desktop Navigation */}
<nav className="hidden md:flex items-center gap-1">
{navItems.map(({ path, label, icon: Icon }) => (
<Link
key={path}
to={path}
className={`flex items-center gap-2 px-4 py-2 rounded-lg text-sm font-medium transition-colors ${
isActive(path)
? 'bg-nifty-50 text-nifty-700'
: 'text-gray-600 hover:bg-gray-50 hover:text-gray-900'
}`}
>
<Icon className="w-4 h-4" />
{label}
</Link>
))}
</nav>
{/* Mobile Menu Button */}
<button
className="md:hidden p-2 rounded-lg hover:bg-gray-100"
onClick={() => setMobileMenuOpen(!mobileMenuOpen)}
>
{mobileMenuOpen ? <X className="w-6 h-6" /> : <Menu className="w-6 h-6" />}
</button>
</div>
{/* Mobile Navigation */}
{mobileMenuOpen && (
<nav className="md:hidden py-4 border-t border-gray-100">
{navItems.map(({ path, label, icon: Icon }) => (
<Link
key={path}
to={path}
onClick={() => setMobileMenuOpen(false)}
className={`flex items-center gap-3 px-4 py-3 rounded-lg text-sm font-medium ${
isActive(path)
? 'bg-nifty-50 text-nifty-700'
: 'text-gray-600 hover:bg-gray-50'
}`}
>
<Icon className="w-5 h-5" />
{label}
</Link>
))}
</nav>
)}
</div>
</header>
);
}

View File

@ -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 (
<span className={`inline-flex items-center gap-1.5 px-3 py-1 rounded-full text-sm font-semibold ${bg} ${text}`}>
<Icon className="w-4 h-4" />
{decision}
</span>
);
}
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 (
<span className={`text-xs px-2 py-0.5 rounded border ${colors[confidence as keyof typeof colors] || colors.MEDIUM}`}>
{confidence} Confidence
</span>
);
}
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 (
<span className={`text-xs ${colors[risk as keyof typeof colors] || colors.MEDIUM}`}>
{risk} Risk
</span>
);
}
export default function StockCard({ stock, showDetails = true }: StockCardProps) {
return (
<Link
to={`/stock/${stock.symbol}`}
className="card-hover p-4 flex items-center justify-between group"
>
<div className="flex-1 min-w-0">
<div className="flex items-center gap-3 mb-1">
<h3 className="font-semibold text-gray-900 truncate">{stock.symbol}</h3>
<DecisionBadge decision={stock.decision} />
</div>
<p className="text-sm text-gray-500 truncate">{stock.company_name}</p>
{showDetails && (
<div className="flex items-center gap-3 mt-2">
<ConfidenceBadge confidence={stock.confidence} />
<RiskBadge risk={stock.risk} />
</div>
)}
</div>
<ChevronRight className="w-5 h-5 text-gray-400 group-hover:text-nifty-600 transition-colors flex-shrink-0" />
</Link>
);
}
export function StockCardCompact({ stock }: { stock: StockAnalysis }) {
return (
<Link
to={`/stock/${stock.symbol}`}
className="flex items-center justify-between p-3 rounded-lg hover:bg-gray-50 transition-colors"
>
<div className="flex items-center gap-3">
<div className={`w-2 h-2 rounded-full ${
stock.decision === 'BUY' ? 'bg-green-500' :
stock.decision === 'SELL' ? 'bg-red-500' : 'bg-amber-500'
}`} />
<div>
<span className="font-medium text-gray-900">{stock.symbol}</span>
<span className="text-gray-400 mx-2">·</span>
<span className="text-sm text-gray-500">{stock.company_name}</span>
</div>
</div>
<DecisionBadge decision={stock.decision} />
</Link>
);
}

View File

@ -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 (
<div className="card p-6">
<div className="flex items-center justify-between mb-6">
<h2 className="section-title">Today's Summary</h2>
<span className="text-sm text-gray-500">
{new Date(date).toLocaleDateString('en-IN', {
weekday: 'long',
year: 'numeric',
month: 'long',
day: 'numeric',
})}
</span>
</div>
<div className="grid grid-cols-2 lg:grid-cols-4 gap-4">
{stats.map(({ label, value, icon: Icon, color, bg, percentage }) => (
<div key={label} className={`${bg} rounded-xl p-4`}>
<div className="flex items-center justify-between mb-2">
<Icon className={`w-5 h-5 ${color}`} />
{percentage && (
<span className={`text-xs font-medium ${color}`}>{percentage}%</span>
)}
</div>
<p className={`text-2xl font-bold ${color}`}>{value}</p>
<p className="text-sm text-gray-600">{label}</p>
</div>
))}
</div>
{/* Progress bar */}
<div className="mt-6">
<div className="flex h-3 rounded-full overflow-hidden">
<div
className="bg-green-500 transition-all duration-500"
style={{ width: `${(buy / total) * 100}%` }}
/>
<div
className="bg-amber-500 transition-all duration-500"
style={{ width: `${(hold / total) * 100}%` }}
/>
<div
className="bg-red-500 transition-all duration-500"
style={{ width: `${(sell / total) * 100}%` }}
/>
</div>
<div className="flex justify-between mt-2 text-xs text-gray-500">
<span>Buy ({buy})</span>
<span>Hold ({hold})</span>
<span>Sell ({sell})</span>
</div>
</div>
</div>
);
}

View File

@ -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 (
<div className="card p-6">
<div className="flex items-center gap-2 mb-6">
<Trophy className="w-6 h-6 text-amber-500" />
<h2 className="section-title">Top Picks</h2>
</div>
<div className="grid gap-4">
{picks.map((pick, index) => (
<Link
key={pick.symbol}
to={`/stock/${pick.symbol}`}
className={`block p-4 rounded-xl border-2 ${bgColors[index]} hover:shadow-md transition-all group`}
>
<div className="flex items-start justify-between mb-2">
<div className="flex items-center gap-3">
<span className="text-2xl">{medals[index]}</span>
<div>
<div className="flex items-center gap-2">
<h3 className="font-bold text-lg text-gray-900">{pick.symbol}</h3>
<span className="badge-buy">
<TrendingUp className="w-3 h-3 mr-1" />
{pick.decision}
</span>
</div>
<p className="text-sm text-gray-600">{pick.company_name}</p>
</div>
</div>
<ChevronRight className="w-5 h-5 text-gray-400 group-hover:text-nifty-600 transition-colors" />
</div>
<p className="text-sm text-gray-700 mt-3 leading-relaxed">{pick.reason}</p>
<div className="mt-3 flex items-center gap-2">
<span className={`text-xs px-2 py-1 rounded-full ${
pick.risk_level === 'LOW' ? 'bg-green-100 text-green-700' :
pick.risk_level === 'HIGH' ? 'bg-red-100 text-red-700' :
'bg-amber-100 text-amber-700'
}`}>
{pick.risk_level} Risk
</span>
</div>
</Link>
))}
</div>
</div>
);
}
interface StocksToAvoidProps {
stocks: StockToAvoid[];
}
export function StocksToAvoid({ stocks }: StocksToAvoidProps) {
return (
<div className="card p-6">
<div className="flex items-center gap-2 mb-6">
<AlertTriangle className="w-6 h-6 text-red-500" />
<h2 className="section-title">Stocks to Avoid</h2>
</div>
<div className="space-y-3">
{stocks.map((stock) => (
<Link
key={stock.symbol}
to={`/stock/${stock.symbol}`}
className="block p-4 rounded-lg bg-red-50 border border-red-100 hover:bg-red-100 transition-colors group"
>
<div className="flex items-center justify-between mb-1">
<div className="flex items-center gap-2">
<span className="font-bold text-red-800">{stock.symbol}</span>
<span className="badge-sell text-xs">SELL</span>
</div>
<ChevronRight className="w-4 h-4 text-red-400 group-hover:text-red-600" />
</div>
<p className="text-sm text-red-700">{stock.reason}</p>
</Link>
))}
</div>
</div>
);
}

View File

@ -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;
}

94
frontend/src/index.css Normal file
View File

@ -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;
}
}

13
frontend/src/main.tsx Normal file
View File

@ -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(
<StrictMode>
<BrowserRouter>
<App />
</BrowserRouter>
</StrictMode>,
);

View File

@ -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<FilterType>('ALL');
if (!recommendation) {
return (
<div className="min-h-[60vh] flex items-center justify-center">
<div className="text-center">
<RefreshCw className="w-12 h-12 text-gray-300 mx-auto mb-4 animate-spin" />
<h2 className="text-xl font-semibold text-gray-700">Loading recommendations...</h2>
</div>
</div>
);
}
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 (
<div className="space-y-8">
{/* Hero Section */}
<section className="text-center py-8">
<h1 className="text-4xl md:text-5xl font-display font-bold text-gray-900 mb-4">
Nifty 50 <span className="gradient-text">AI Recommendations</span>
</h1>
<p className="text-lg text-gray-600 max-w-2xl mx-auto">
AI-powered daily stock analysis for all Nifty 50 stocks. Get actionable buy, sell, and hold recommendations.
</p>
<div className="flex items-center justify-center gap-2 mt-4 text-sm text-gray-500">
<Calendar className="w-4 h-4" />
<span>Last updated: {new Date(recommendation.date).toLocaleDateString('en-IN', {
weekday: 'long',
year: 'numeric',
month: 'long',
day: 'numeric',
})}</span>
</div>
</section>
{/* Summary Stats */}
<SummaryStats
total={recommendation.summary.total}
buy={recommendation.summary.buy}
sell={recommendation.summary.sell}
hold={recommendation.summary.hold}
date={recommendation.date}
/>
{/* Chart and Stats Section */}
<div className="grid lg:grid-cols-2 gap-6">
<div className="card p-6">
<div className="flex items-center gap-2 mb-4">
<PieChart className="w-5 h-5 text-nifty-600" />
<h2 className="section-title text-lg">Decision Distribution</h2>
</div>
<SummaryPieChart
buy={recommendation.summary.buy}
sell={recommendation.summary.sell}
hold={recommendation.summary.hold}
/>
</div>
<div className="card p-6">
<h2 className="section-title text-lg mb-4">Quick Analysis</h2>
<div className="space-y-4">
<div className="p-4 bg-green-50 rounded-lg">
<div className="flex justify-between items-center mb-2">
<span className="font-medium text-green-800">Bullish Signals</span>
<span className="text-2xl font-bold text-green-600">{recommendation.summary.buy}</span>
</div>
<p className="text-sm text-green-700">
{((recommendation.summary.buy / recommendation.summary.total) * 100).toFixed(0)}% of stocks show buying opportunities
</p>
</div>
<div className="p-4 bg-amber-50 rounded-lg">
<div className="flex justify-between items-center mb-2">
<span className="font-medium text-amber-800">Neutral Position</span>
<span className="text-2xl font-bold text-amber-600">{recommendation.summary.hold}</span>
</div>
<p className="text-sm text-amber-700">
{((recommendation.summary.hold / recommendation.summary.total) * 100).toFixed(0)}% of stocks recommend holding
</p>
</div>
<div className="p-4 bg-red-50 rounded-lg">
<div className="flex justify-between items-center mb-2">
<span className="font-medium text-red-800">Bearish Signals</span>
<span className="text-2xl font-bold text-red-600">{recommendation.summary.sell}</span>
</div>
<p className="text-sm text-red-700">
{((recommendation.summary.sell / recommendation.summary.total) * 100).toFixed(0)}% of stocks suggest selling
</p>
</div>
</div>
</div>
</div>
{/* Top Picks and Avoid Section */}
<div className="grid lg:grid-cols-2 gap-6">
<TopPicks picks={recommendation.top_picks} />
<StocksToAvoid stocks={recommendation.stocks_to_avoid} />
</div>
{/* All Stocks Section */}
<section className="card">
<div className="p-6 border-b border-gray-100">
<div className="flex flex-col sm:flex-row sm:items-center justify-between gap-4">
<div className="flex items-center gap-2">
<Filter className="w-5 h-5 text-gray-400" />
<h2 className="section-title">All Stocks</h2>
</div>
<div className="flex gap-2">
{filterButtons.map(({ label, value, count }) => (
<button
key={value}
onClick={() => setFilter(value)}
className={`px-3 py-1.5 text-sm font-medium rounded-lg transition-colors ${
filter === value
? 'bg-nifty-600 text-white'
: 'bg-gray-100 text-gray-600 hover:bg-gray-200'
}`}
>
{label} ({count})
</button>
))}
</div>
</div>
</div>
<div className="divide-y divide-gray-100">
{filteredStocks.map((stock) => (
<StockCard key={stock.symbol} stock={stock} />
))}
</div>
{filteredStocks.length === 0 && (
<div className="p-12 text-center">
<p className="text-gray-500">No stocks match the selected filter.</p>
</div>
)}
</section>
{/* CTA Section */}
<section className="card bg-gradient-to-r from-nifty-600 to-nifty-800 text-white p-8">
<div className="flex flex-col md:flex-row items-center justify-between gap-6">
<div>
<h2 className="text-2xl font-display font-bold mb-2">
Track Historical Recommendations
</h2>
<p className="text-nifty-100">
View past recommendations and track how our AI predictions performed over time.
</p>
</div>
<Link
to="/history"
className="inline-flex items-center gap-2 bg-white text-nifty-700 px-6 py-3 rounded-lg font-semibold hover:bg-nifty-50 transition-colors"
>
View History
<ChevronRight className="w-5 h-5" />
</Link>
</div>
</section>
</div>
);
}

View File

@ -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<string | null>(null);
const dates = sampleRecommendations.map(r => r.date);
const getRecommendation = (date: string) => {
return sampleRecommendations.find(r => r.date === date);
};
return (
<div className="space-y-8">
{/* Header */}
<section className="text-center py-8">
<h1 className="text-4xl font-display font-bold text-gray-900 mb-4">
Historical <span className="gradient-text">Recommendations</span>
</h1>
<p className="text-lg text-gray-600 max-w-2xl mx-auto">
Browse past AI recommendations and track performance over time.
</p>
</section>
{/* Date Selector */}
<div className="card p-6">
<div className="flex items-center gap-2 mb-4">
<Calendar className="w-5 h-5 text-nifty-600" />
<h2 className="font-semibold text-gray-900">Select Date</h2>
</div>
<div className="flex flex-wrap gap-2">
{dates.map((date) => {
const rec = getRecommendation(date);
return (
<button
key={date}
onClick={() => setSelectedDate(selectedDate === date ? null : date)}
className={`px-4 py-2 rounded-lg text-sm font-medium transition-all ${
selectedDate === date
? 'bg-nifty-600 text-white'
: 'bg-gray-100 text-gray-700 hover:bg-gray-200'
}`}
>
<div>{new Date(date).toLocaleDateString('en-IN', { month: 'short', day: 'numeric' })}</div>
<div className="text-xs opacity-75 mt-0.5">
{rec?.summary.buy}B / {rec?.summary.sell}S / {rec?.summary.hold}H
</div>
</button>
);
})}
</div>
</div>
{/* Selected Date Details */}
{selectedDate && (
<div className="card">
<div className="p-6 border-b border-gray-100">
<div className="flex items-center justify-between">
<h2 className="section-title">
{new Date(selectedDate).toLocaleDateString('en-IN', {
weekday: 'long',
year: 'numeric',
month: 'long',
day: 'numeric',
})}
</h2>
<div className="flex items-center gap-4 text-sm">
<span className="flex items-center gap-1 text-green-600">
<TrendingUp className="w-4 h-4" />
{getRecommendation(selectedDate)?.summary.buy} Buy
</span>
<span className="flex items-center gap-1 text-red-600">
<TrendingDown className="w-4 h-4" />
{getRecommendation(selectedDate)?.summary.sell} Sell
</span>
<span className="flex items-center gap-1 text-amber-600">
<Minus className="w-4 h-4" />
{getRecommendation(selectedDate)?.summary.hold} Hold
</span>
</div>
</div>
</div>
<div className="divide-y divide-gray-100">
{Object.values(getRecommendation(selectedDate)?.analysis || {}).map((stock) => (
<Link
key={stock.symbol}
to={`/stock/${stock.symbol}`}
className="flex items-center justify-between p-4 hover:bg-gray-50 transition-colors group"
>
<div>
<div className="flex items-center gap-3">
<span className="font-semibold text-gray-900">{stock.symbol}</span>
<DecisionBadge decision={stock.decision} />
</div>
<p className="text-sm text-gray-500 mt-1">{stock.company_name}</p>
</div>
<ChevronRight className="w-5 h-5 text-gray-400 group-hover:text-nifty-600" />
</Link>
))}
</div>
</div>
)}
{/* Summary Cards */}
<div className="grid md:grid-cols-3 gap-6">
<div className="card p-6 text-center">
<BarChart3 className="w-12 h-12 text-nifty-600 mx-auto mb-4" />
<h3 className="text-3xl font-bold text-gray-900">{dates.length}</h3>
<p className="text-gray-600">Days of Analysis</p>
</div>
<div className="card p-6 text-center">
<TrendingUp className="w-12 h-12 text-green-600 mx-auto mb-4" />
<h3 className="text-3xl font-bold text-gray-900">
{sampleRecommendations.reduce((acc, r) => acc + r.summary.buy, 0)}
</h3>
<p className="text-gray-600">Total Buy Signals</p>
</div>
<div className="card p-6 text-center">
<TrendingDown className="w-12 h-12 text-red-600 mx-auto mb-4" />
<h3 className="text-3xl font-bold text-gray-900">
{sampleRecommendations.reduce((acc, r) => acc + r.summary.sell, 0)}
</h3>
<p className="text-gray-600">Total Sell Signals</p>
</div>
</div>
</div>
);
}

View File

@ -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 (
<div className="min-h-[60vh] flex items-center justify-center">
<div className="text-center">
<AlertTriangle className="w-12 h-12 text-amber-500 mx-auto mb-4" />
<h2 className="text-xl font-semibold text-gray-700 mb-2">Stock Not Found</h2>
<p className="text-gray-500 mb-4">The stock "{symbol}" was not found in Nifty 50.</p>
<Link to="/stocks" className="btn-primary">
View All Stocks
</Link>
</div>
</div>
);
}
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 (
<div className="space-y-8">
{/* Back Button */}
<div>
<Link
to="/stocks"
className="inline-flex items-center gap-2 text-gray-600 hover:text-nifty-600 transition-colors"
>
<ArrowLeft className="w-4 h-4" />
Back to All Stocks
</Link>
</div>
{/* Stock Header */}
<section className="card overflow-hidden">
<div className={`bg-gradient-to-r ${bgGradient} p-6 text-white`}>
<div className="flex items-start justify-between">
<div>
<div className="flex items-center gap-3 mb-2">
<h1 className="text-3xl font-display font-bold">{stock.symbol}</h1>
{analysis?.decision && (
<span className="inline-flex items-center gap-1.5 px-3 py-1 rounded-full text-sm font-semibold bg-white/20 backdrop-blur-sm">
<DecisionIcon className="w-4 h-4" />
{analysis.decision}
</span>
)}
</div>
<p className="text-white/90 text-lg">{stock.company_name}</p>
<div className="flex items-center gap-2 mt-3 text-white/80">
<Building2 className="w-4 h-4" />
<span>{stock.sector || 'N/A'}</span>
</div>
</div>
<div className="text-right">
<div className="text-sm text-white/80 mb-1">Latest Analysis</div>
<div className="flex items-center gap-2 text-white/90">
<Calendar className="w-4 h-4" />
{latestRecommendation?.date ? new Date(latestRecommendation.date).toLocaleDateString('en-IN', {
month: 'short',
day: 'numeric',
year: 'numeric',
}) : 'N/A'}
</div>
</div>
</div>
</div>
{/* Analysis Details */}
<div className="p-6">
{analysis ? (
<div className="grid md:grid-cols-3 gap-6">
<div className="space-y-2">
<h3 className="text-sm font-medium text-gray-500 uppercase tracking-wide">Decision</h3>
<DecisionBadge decision={analysis.decision} />
</div>
<div className="space-y-2">
<h3 className="text-sm font-medium text-gray-500 uppercase tracking-wide">Confidence</h3>
<ConfidenceBadge confidence={analysis.confidence} />
</div>
<div className="space-y-2">
<h3 className="text-sm font-medium text-gray-500 uppercase tracking-wide">Risk Level</h3>
<RiskBadge risk={analysis.risk} />
</div>
</div>
) : (
<div className="flex items-center gap-3 text-gray-500">
<Info className="w-5 h-5" />
<span>No analysis available for this stock yet.</span>
</div>
)}
</div>
</section>
{/* Quick Stats Grid */}
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
<div className="card p-4 text-center">
<div className="text-2xl font-bold text-gray-900">{history.length}</div>
<div className="text-sm text-gray-500">Total Analyses</div>
</div>
<div className="card p-4 text-center">
<div className="text-2xl font-bold text-green-600">
{history.filter(h => h.decision === 'BUY').length}
</div>
<div className="text-sm text-gray-500">Buy Signals</div>
</div>
<div className="card p-4 text-center">
<div className="text-2xl font-bold text-amber-600">
{history.filter(h => h.decision === 'HOLD').length}
</div>
<div className="text-sm text-gray-500">Hold Signals</div>
</div>
<div className="card p-4 text-center">
<div className="text-2xl font-bold text-red-600">
{history.filter(h => h.decision === 'SELL').length}
</div>
<div className="text-sm text-gray-500">Sell Signals</div>
</div>
</div>
{/* Analysis History */}
<section className="card">
<div className="p-6 border-b border-gray-100">
<h2 className="section-title">Recommendation History</h2>
</div>
{history.length > 0 ? (
<div className="divide-y divide-gray-100">
{history.map((entry, idx) => (
<div key={idx} className="p-4 flex items-center justify-between">
<div className="flex items-center gap-4">
<div className="text-sm text-gray-500">
{new Date(entry.date).toLocaleDateString('en-IN', {
weekday: 'short',
month: 'short',
day: 'numeric',
year: 'numeric',
})}
</div>
</div>
<DecisionBadge decision={entry.decision} />
</div>
))}
</div>
) : (
<div className="p-12 text-center">
<Calendar className="w-12 h-12 text-gray-300 mx-auto mb-4" />
<h3 className="text-lg font-semibold text-gray-700 mb-2">No History Yet</h3>
<p className="text-gray-500">Recommendation history will appear here as we analyze this stock daily.</p>
</div>
)}
</section>
{/* Top Pick / Avoid Status */}
{latestRecommendation && (
<>
{latestRecommendation.top_picks.some(p => p.symbol === symbol) && (
<section className="card bg-gradient-to-r from-green-50 to-emerald-50 border-green-200">
<div className="p-6">
<div className="flex items-start gap-4">
<div className="w-12 h-12 rounded-xl bg-green-100 flex items-center justify-center">
<TrendingUp className="w-6 h-6 text-green-600" />
</div>
<div>
<h3 className="text-lg font-semibold text-green-800 mb-2">Top Pick</h3>
<p className="text-green-700">
{latestRecommendation.top_picks.find(p => p.symbol === symbol)?.reason}
</p>
</div>
</div>
</div>
</section>
)}
{latestRecommendation.stocks_to_avoid.some(s => s.symbol === symbol) && (
<section className="card bg-gradient-to-r from-red-50 to-rose-50 border-red-200">
<div className="p-6">
<div className="flex items-start gap-4">
<div className="w-12 h-12 rounded-xl bg-red-100 flex items-center justify-center">
<AlertTriangle className="w-6 h-6 text-red-600" />
</div>
<div>
<h3 className="text-lg font-semibold text-red-800 mb-2">Stock to Avoid</h3>
<p className="text-red-700">
{latestRecommendation.stocks_to_avoid.find(s => s.symbol === symbol)?.reason}
</p>
</div>
</div>
</div>
</section>
)}
</>
)}
{/* Disclaimer */}
<section className="card p-6 bg-gray-50">
<div className="flex items-start gap-3">
<Info className="w-5 h-5 text-gray-400 flex-shrink-0 mt-0.5" />
<p className="text-sm text-gray-600">
<strong>Disclaimer:</strong> 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.
</p>
</div>
</section>
</div>
);
}

View File

@ -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<string>('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 (
<div className="space-y-8">
{/* Header */}
<section className="text-center py-8">
<h1 className="text-4xl font-display font-bold text-gray-900 mb-4">
All <span className="gradient-text">Nifty 50 Stocks</span>
</h1>
<p className="text-lg text-gray-600 max-w-2xl mx-auto">
Browse all 50 stocks in the Nifty index with their latest AI recommendations.
</p>
</section>
{/* Search and Filter */}
<div className="card p-6">
<div className="flex flex-col md:flex-row gap-4">
{/* Search */}
<div className="relative flex-1">
<Search className="absolute left-3 top-1/2 -translate-y-1/2 w-5 h-5 text-gray-400" />
<input
type="text"
placeholder="Search by symbol or company name..."
value={search}
onChange={(e) => 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"
/>
</div>
{/* Sector Filter */}
<div className="flex items-center gap-2">
<Filter className="w-5 h-5 text-gray-400" />
<select
value={sectorFilter}
onChange={(e) => setSectorFilter(e.target.value)}
className="px-4 py-2.5 rounded-lg border border-gray-200 focus:border-nifty-500 focus:ring-2 focus:ring-nifty-500/20 outline-none bg-white"
>
{sectors.map((sector) => (
<option key={sector} value={sector}>
{sector === 'ALL' ? 'All Sectors' : sector}
</option>
))}
</select>
</div>
</div>
<p className="text-sm text-gray-500 mt-4">
Showing {filteredStocks.length} of {NIFTY_50_STOCKS.length} stocks
</p>
</div>
{/* Stocks Grid */}
<div className="grid md:grid-cols-2 lg:grid-cols-3 gap-4">
{filteredStocks.map((stock) => {
const analysis = getStockAnalysis(stock.symbol);
return (
<Link
key={stock.symbol}
to={`/stock/${stock.symbol}`}
className="card-hover p-4 group"
>
<div className="flex items-start justify-between mb-3">
<div>
<h3 className="font-bold text-lg text-gray-900">{stock.symbol}</h3>
<p className="text-sm text-gray-500">{stock.company_name}</p>
</div>
<ChevronRight className="w-5 h-5 text-gray-400 group-hover:text-nifty-600 transition-colors" />
</div>
<div className="flex items-center gap-2 mb-3">
<Building2 className="w-4 h-4 text-gray-400" />
<span className="text-sm text-gray-600">{stock.sector}</span>
</div>
{analysis && (
<div className="pt-3 border-t border-gray-100">
<div className="flex items-center justify-between">
<DecisionBadge decision={analysis.decision} />
<div className="flex items-center gap-2">
<ConfidenceBadge confidence={analysis.confidence} />
</div>
</div>
</div>
)}
</Link>
);
})}
</div>
{filteredStocks.length === 0 && (
<div className="card p-12 text-center">
<Search className="w-12 h-12 text-gray-300 mx-auto mb-4" />
<h3 className="text-lg font-semibold text-gray-700 mb-2">No stocks found</h3>
<p className="text-gray-500">Try adjusting your search or filter criteria.</p>
</div>
)}
</div>
);
}

View File

@ -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<string, StockAnalysis>;
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<T>(endpoint: string, options?: RequestInit): Promise<T> {
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<DailyRecommendation> {
return this.fetch('/recommendations/latest');
}
/**
* Get recommendation for a specific date
*/
async getRecommendationByDate(date: string): Promise<DailyRecommendation> {
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<string, StockAnalysis>;
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;

130
frontend/src/types/index.ts Normal file
View File

@ -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<string, StockAnalysis>;
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' },
];

View File

@ -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: [],
}

View File

@ -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"]
}

7
frontend/tsconfig.json Normal file
View File

@ -0,0 +1,7 @@
{
"files": [],
"references": [
{ "path": "./tsconfig.app.json" },
{ "path": "./tsconfig.node.json" }
]
}

View File

@ -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"]
}

16
frontend/vite.config.ts Normal file
View File

@ -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',
},
},
})

184
nifty50_recommend.py Normal file
View File

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

23
package.json Normal file
View File

@ -0,0 +1,23 @@
{
"name": "tradingagents",
"version": "1.0.0",
"description": "<p align=\"center\"> <img src=\"assets/TauricResearch.png\" style=\"width: 60%; height: auto;\"> </p>",
"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"
}
}

View File

@ -24,3 +24,5 @@ rich
questionary
langchain_anthropic
langchain-google-genai
jugaad-data
sentence-transformers

View File

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

View File

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

View File

@ -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"<system>\n{msg.content}\n</system>\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()

View File

@ -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}"
# 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}"

View File

@ -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(',')]

View File

@ -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)}")

View File

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

View File

@ -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)}"
return f"Error retrieving insider transactions for {normalized_ticker}: {str(e)}"

View File

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

View File

@ -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"])

View File

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

View File

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