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:
parent
7902d249ca
commit
e43acb8247
|
|
@ -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/
|
||||
|
|
|
|||
57
cli/main.py
57
cli/main.py
|
|
@ -26,6 +26,7 @@ from rich.rule import Rule
|
|||
|
||||
from tradingagents.graph.trading_graph import TradingAgentsGraph
|
||||
from tradingagents.default_config import DEFAULT_CONFIG
|
||||
from tradingagents.dataflows.markets import is_nifty_50_stock, NIFTY_50_STOCKS
|
||||
from cli.models import AnalystType
|
||||
from cli.utils import *
|
||||
|
||||
|
|
@ -429,29 +430,42 @@ def get_user_selections():
|
|||
box_content += f"\n[dim]Default: {default}[/dim]"
|
||||
return Panel(box_content, border_style="blue", padding=(1, 2))
|
||||
|
||||
# Step 1: Ticker symbol
|
||||
# Step 1: Market selection
|
||||
console.print(
|
||||
create_question_box(
|
||||
"Step 1: Ticker Symbol", "Enter the ticker symbol to analyze", "SPY"
|
||||
"Step 1: Market Selection", "Select the market for your analysis"
|
||||
)
|
||||
)
|
||||
selected_ticker = get_ticker()
|
||||
selected_market = select_market()
|
||||
|
||||
# Step 2: Analysis date
|
||||
# Show Nifty 50 stocks if Indian market is selected
|
||||
if selected_market == "india_nse":
|
||||
show_nifty_50_stocks()
|
||||
|
||||
# Step 2: Ticker symbol
|
||||
console.print(
|
||||
create_question_box(
|
||||
"Step 2: Ticker Symbol", "Enter the ticker symbol to analyze",
|
||||
"RELIANCE" if selected_market == "india_nse" else "SPY"
|
||||
)
|
||||
)
|
||||
selected_ticker = get_ticker_with_market_hint(selected_market)
|
||||
|
||||
# Step 3: Analysis date
|
||||
default_date = datetime.datetime.now().strftime("%Y-%m-%d")
|
||||
console.print(
|
||||
create_question_box(
|
||||
"Step 2: Analysis Date",
|
||||
"Step 3: Analysis Date",
|
||||
"Enter the analysis date (YYYY-MM-DD)",
|
||||
default_date,
|
||||
)
|
||||
)
|
||||
analysis_date = get_analysis_date()
|
||||
|
||||
# Step 3: Select analysts
|
||||
# Step 4: Select analysts
|
||||
console.print(
|
||||
create_question_box(
|
||||
"Step 3: Analysts Team", "Select your LLM analyst agents for the analysis"
|
||||
"Step 4: Analysts Team", "Select your LLM analyst agents for the analysis"
|
||||
)
|
||||
)
|
||||
selected_analysts = select_analysts()
|
||||
|
|
@ -459,26 +473,26 @@ def get_user_selections():
|
|||
f"[green]Selected analysts:[/green] {', '.join(analyst.value for analyst in selected_analysts)}"
|
||||
)
|
||||
|
||||
# Step 4: Research depth
|
||||
# Step 5: Research depth
|
||||
console.print(
|
||||
create_question_box(
|
||||
"Step 4: Research Depth", "Select your research depth level"
|
||||
"Step 5: Research Depth", "Select your research depth level"
|
||||
)
|
||||
)
|
||||
selected_research_depth = select_research_depth()
|
||||
|
||||
# Step 5: OpenAI backend
|
||||
# Step 6: OpenAI backend
|
||||
console.print(
|
||||
create_question_box(
|
||||
"Step 5: OpenAI backend", "Select which service to talk to"
|
||||
"Step 6: LLM Provider", "Select which service to talk to"
|
||||
)
|
||||
)
|
||||
selected_llm_provider, backend_url = select_llm_provider()
|
||||
|
||||
# Step 6: Thinking agents
|
||||
# Step 7: Thinking agents
|
||||
console.print(
|
||||
create_question_box(
|
||||
"Step 6: Thinking Agents", "Select your thinking agents for analysis"
|
||||
"Step 7: Thinking Agents", "Select your thinking agents for analysis"
|
||||
)
|
||||
)
|
||||
selected_shallow_thinker = select_shallow_thinking_agent(selected_llm_provider)
|
||||
|
|
@ -493,6 +507,7 @@ def get_user_selections():
|
|||
"backend_url": backend_url,
|
||||
"shallow_thinker": selected_shallow_thinker,
|
||||
"deep_thinker": selected_deep_thinker,
|
||||
"market": selected_market,
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -747,6 +762,13 @@ def run_analysis():
|
|||
config["deep_think_llm"] = selections["deep_thinker"]
|
||||
config["backend_url"] = selections["backend_url"]
|
||||
config["llm_provider"] = selections["llm_provider"].lower()
|
||||
config["market"] = selections["market"]
|
||||
|
||||
# Display market info for NSE stocks
|
||||
if is_nifty_50_stock(selections["ticker"]):
|
||||
company_name = NIFTY_50_STOCKS.get(selections["ticker"].replace(".NS", ""), "")
|
||||
console.print(f"[cyan]Analyzing NSE stock:[/cyan] {selections['ticker']} - {company_name}")
|
||||
console.print("[dim]Using jugaad-data for NSE stock data, yfinance for fundamentals[/dim]")
|
||||
|
||||
# Initialize the graph
|
||||
graph = TradingAgentsGraph(
|
||||
|
|
@ -808,10 +830,17 @@ def run_analysis():
|
|||
update_display(layout)
|
||||
|
||||
# Add initial messages
|
||||
message_buffer.add_message("System", f"Selected ticker: {selections['ticker']}")
|
||||
ticker_info = selections['ticker']
|
||||
if is_nifty_50_stock(selections['ticker']):
|
||||
company_name = NIFTY_50_STOCKS.get(selections['ticker'].replace(".NS", ""), "")
|
||||
ticker_info = f"{selections['ticker']} ({company_name}) [NSE]"
|
||||
message_buffer.add_message("System", f"Selected ticker: {ticker_info}")
|
||||
message_buffer.add_message(
|
||||
"System", f"Analysis date: {selections['analysis_date']}"
|
||||
)
|
||||
message_buffer.add_message(
|
||||
"System", f"Market: {selections['market'].upper()}"
|
||||
)
|
||||
message_buffer.add_message(
|
||||
"System",
|
||||
f"Selected analysts: {', '.join(analyst.value for analyst in selections['analysts'])}",
|
||||
|
|
|
|||
118
cli/utils.py
118
cli/utils.py
|
|
@ -1,7 +1,13 @@
|
|||
import questionary
|
||||
from typing import List, Optional, Tuple, Dict
|
||||
from rich.console import Console
|
||||
from rich.table import Table
|
||||
from rich import box
|
||||
|
||||
from cli.models import AnalystType
|
||||
from tradingagents.dataflows.markets import NIFTY_50_STOCKS, is_nifty_50_stock
|
||||
|
||||
console = Console()
|
||||
|
||||
ANALYST_ORDER = [
|
||||
("Market Analyst", AnalystType.MARKET),
|
||||
|
|
@ -274,3 +280,115 @@ def select_llm_provider() -> tuple[str, str]:
|
|||
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
|
||||
|
|
|
|||
|
|
@ -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?
|
||||
|
|
@ -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...
|
||||
},
|
||||
},
|
||||
])
|
||||
```
|
||||
|
|
@ -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.
|
|
@ -0,0 +1,3 @@
|
|||
fastapi>=0.109.0
|
||||
uvicorn>=0.27.0
|
||||
pydantic>=2.0.0
|
||||
|
|
@ -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()
|
||||
|
|
@ -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)
|
||||
|
|
@ -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,
|
||||
},
|
||||
},
|
||||
])
|
||||
|
|
@ -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>
|
||||
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
export default {
|
||||
plugins: {
|
||||
'@tailwindcss/postcss': {},
|
||||
autoprefixer: {},
|
||||
},
|
||||
}
|
||||
|
|
@ -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 |
|
|
@ -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 |
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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;
|
||||
|
|
@ -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 |
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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>,
|
||||
);
|
||||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
@ -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;
|
||||
|
|
@ -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' },
|
||||
];
|
||||
|
|
@ -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: [],
|
||||
}
|
||||
|
|
@ -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"]
|
||||
}
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
{
|
||||
"files": [],
|
||||
"references": [
|
||||
{ "path": "./tsconfig.app.json" },
|
||||
{ "path": "./tsconfig.node.json" }
|
||||
]
|
||||
}
|
||||
|
|
@ -0,0 +1,26 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
|
||||
"target": "ES2023",
|
||||
"lib": ["ES2023"],
|
||||
"module": "ESNext",
|
||||
"types": ["node"],
|
||||
"skipLibCheck": true,
|
||||
|
||||
/* Bundler mode */
|
||||
"moduleResolution": "bundler",
|
||||
"allowImportingTsExtensions": true,
|
||||
"verbatimModuleSyntax": true,
|
||||
"moduleDetection": "force",
|
||||
"noEmit": true,
|
||||
|
||||
/* Linting */
|
||||
"strict": true,
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"erasableSyntaxOnly": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"noUncheckedSideEffectImports": true
|
||||
},
|
||||
"include": ["vite.config.ts"]
|
||||
}
|
||||
|
|
@ -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',
|
||||
},
|
||||
},
|
||||
})
|
||||
|
|
@ -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()
|
||||
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
@ -24,3 +24,5 @@ rich
|
|||
questionary
|
||||
langchain_anthropic
|
||||
langchain-google-genai
|
||||
jugaad-data
|
||||
sentence-transformers
|
||||
|
|
|
|||
|
|
@ -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 = [
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
@ -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}"
|
||||
|
|
@ -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(',')]
|
||||
|
|
|
|||
|
|
@ -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)}")
|
||||
|
|
@ -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()
|
||||
|
|
@ -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,7 +336,9 @@ 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
|
||||
|
|
@ -338,19 +346,19 @@ def get_cashflow(
|
|||
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,7 +368,9 @@ 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
|
||||
|
|
@ -368,19 +378,19 @@ def get_income_statement(
|
|||
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)}"
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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"])
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
Loading…
Reference in New Issue