TradingAgents/tradingagents/nifty50_recommender.py

449 lines
14 KiB
Python

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