Spaces:
Sleeping
Sleeping
| import yfinance as yf | |
| import requests | |
| from bs4 import BeautifulSoup | |
| import pandas as pd | |
| import numpy as np | |
| from langchain_openai import ChatOpenAI | |
| from langchain_groq import ChatGroq | |
| import os | |
| import pytz | |
| from typing import Dict, Any | |
| from state import TraderState | |
| from datetime import datetime, timedelta | |
| # os.environ["OPENAI_API_KEY"] = os.getenv("OPENROUTER_API_KEY", "") | |
| # llm = ChatOpenAI(model="openai/gpt-3.5-turbo", openai_api_base="https://openrouter.ai/api/v1", temperature=0.2) | |
| llm = ChatGroq(model = "moonshotai/kimi-k2-instruct-0905",api_key=os.getenv("GROQ_API_KEY"), temperature=0.0) | |
| ALPHA_VANTAGE_KEY = os.getenv("ALPHA_VANTAGE_API_KEY", "") | |
| NEWSAPI_KEY = os.getenv("NEWSAPI_KEY", "") | |
| def fetch_data(symbol: str, horizon: str) -> pd.DataFrame: | |
| """Fetch data precisely for each trading type""" | |
| if horizon == "intraday": | |
| period = "1d" | |
| interval = "1m" | |
| elif horizon == "scalping": | |
| period = "1d" | |
| interval = "1m" | |
| elif horizon == "swing": | |
| period = "3mo" | |
| interval = "1d" | |
| elif horizon == "momentum": | |
| period = "6mo" | |
| interval = "1d" | |
| else: # long_term | |
| period = "2y" | |
| interval = "1d" | |
| try: | |
| stock = yf.Ticker(symbol) | |
| hist = stock.history(period=period, interval=interval) | |
| if hist.empty or len(hist) < 50: | |
| hist = stock.history(period="1y", interval="1d") # Fallback | |
| if hist.empty: | |
| raise ValueError(f"No data from yfinance for {symbol}") | |
| eastern = pytz.timezone('US/Eastern') | |
| ist = pytz.timezone('Asia/Kolkata') | |
| hist.index = hist.index.tz_convert(eastern).tz_convert(ist) | |
| print(f"Data fetched for {symbol} ({horizon}): {len(hist)} rows ({interval} intervals)") | |
| return hist | |
| except Exception as e: | |
| print(f"Yfinance failed for {symbol}: {e}") | |
| return pd.DataFrame() | |
| def compute_indicators(hist: pd.DataFrame, horizon: str) -> Dict[str, Any]: | |
| if hist.empty: | |
| return {"error": "No data available", "current_price": 0, "rsi": 50, "macd": 0, "sma_20": 0, "ema_50": 0, "bb_upper": 0, "bb_lower": 0, "volume_avg": 0} | |
| try: | |
| hist = hist.sort_index() | |
| close = hist['Close'] | |
| if len(close) < 50: | |
| return {"error": "Insufficient data"} | |
| delta = close.diff() | |
| gain = (delta.where(delta > 0, 0)).rolling(14).mean() | |
| loss = (-delta.where(delta < 0, 0)).rolling(14).mean() | |
| rs = gain / loss | |
| rsi = 100 - (100 / (1 + rs)).iloc[-1] | |
| ema12 = close.ewm(12).mean() | |
| ema26 = close.ewm(26).mean() | |
| macd = ema12 - ema26 | |
| macd_value = macd.iloc[-1] | |
| sma_20 = close.rolling(20).mean().iloc[-1] | |
| ema_50 = close.ewm(50).mean().iloc[-1] | |
| if len(close) >= 20: | |
| std_bb = close.rolling(20).std() | |
| bb_upper = (sma_20 + 2 * std_bb).iloc[-1] | |
| bb_lower = (sma_20 - 2 * std_bb).iloc[-1] | |
| else: | |
| bb_upper = bb_lower = None | |
| volume_avg = hist['Volume'].rolling(20).mean().iloc[-1] if 'Volume' in hist else 0 | |
| return { | |
| "current_price": close.iloc[-1], "rsi": rsi, "macd": macd_value, | |
| "sma_20": sma_20, "ema_50": ema_50, "bb_upper": bb_upper, "bb_lower": bb_lower, | |
| "volume_avg": volume_avg | |
| } | |
| except Exception as e: | |
| return {"error": str(e)} | |
| def fetch_news_sentiment(symbol: str) -> dict: | |
| """ | |
| Fetch recent news from NewsAPI (free forever) and analyze sentiment. | |
| """ | |
| try: | |
| base_symbol = symbol.replace(".NS", "").replace(".BO", "") | |
| query = f'"{base_symbol}" OR "{base_symbol} stock"' | |
| url = f"https://newsapi.org/v2/everything?q={query}&sortBy=publishedAt&apiKey={NEWSAPI_KEY}&pageSize=10&language=en" | |
| response = requests.get(url, timeout=10) | |
| data = response.json() | |
| if 'articles' in data and data['articles']: | |
| articles = data['articles'][:5] | |
| headlines = [art['title'] for art in articles] | |
| descriptions = [art.get('description', '') for art in articles] | |
| positive_words = ['gain', 'rise', 'profit', 'growth', 'earnings', 'bullish', 'up', 'positive', 'strong'] | |
| negative_words = ['loss', 'fall', 'decline', 'drop', 'bearish', 'down', 'negative', 'weak', 'crash'] | |
| pos_count = sum(1 for h in headlines + descriptions for word in positive_words if word in h.lower()) | |
| neg_count = sum(1 for h in headlines + descriptions for word in negative_words if word in h.lower()) | |
| sentiment = 'positive' if pos_count > neg_count else 'negative' if neg_count > pos_count else 'neutral' | |
| summary = f"Recent news sentiment: {sentiment}. Key headlines: {'; '.join(headlines[:3])}." | |
| return {'sentiment': sentiment, 'headlines': headlines, 'summary': summary} | |
| else: | |
| return {'sentiment': 'neutral', 'headlines': ["No recent news found."], 'summary': "No recent news available; sentiment neutral."} | |
| except Exception as e: | |
| print(f"NewsAPI error: {e}") | |
| return {'sentiment': 'neutral', 'headlines': ["News fetch failed."], 'summary': "News unavailable; assuming neutral sentiment."} | |
| def summarize_features(indicators: Dict[str, Any], hist: pd.DataFrame, news: list, action: str, symbol: str, news_sentiment: str = 'neutral') -> str: | |
| if indicators.get("error"): | |
| try: | |
| stock = yf.Ticker(symbol) | |
| info = stock.info | |
| price = info.get('currentPrice', 0) | |
| change = info.get('regularMarketChangePercent', 0) | |
| signals = [f"Recent change: {change:.1f}%"] | |
| if change > 0: | |
| signals.append("Positive momentum") | |
| else: | |
| signals.append("Negative momentum") | |
| return f"Fallback Signals: {', '.join(signals)}; News: {news[0] if news else 'Neutral'}" | |
| except: | |
| return "No reliable data; default to caution." | |
| rsi = indicators.get('rsi', 50) | |
| macd = indicators.get('macd', 0) | |
| price = indicators.get('current_price', 0) | |
| sma_20 = indicators.get('sma_20', price) | |
| bb_upper = indicators.get('bb_upper', price * 1.1) | |
| bb_lower = indicators.get('bb_lower', price * 0.9) | |
| recent_prices = hist['Close'].tail(10) if not hist.empty else pd.Series([price]) | |
| trend = "upward" if recent_prices.iloc[-1] > recent_prices.iloc[0] else "downward" | |
| change_pct = ((recent_prices.iloc[-1] - recent_prices.iloc[0]) / recent_prices.iloc[0]) * 100 if len(recent_prices) > 1 else 0 | |
| signals = [] | |
| if rsi < 30: | |
| signals.append("Oversold RSI (potential buy reversal)") | |
| elif rsi > 70: | |
| signals.append("Overbought RSI (sell risk)") | |
| if macd > 0: | |
| signals.append("Bullish MACD") | |
| else: | |
| signals.append("Bearish MACD") | |
| if price < bb_lower: | |
| signals.append("Price near support (buy opportunity)") | |
| elif price > bb_upper: | |
| signals.append("Price near resistance (sell signal)") | |
| if price > sma_20: | |
| signals.append("Above SMA20 (momentum)") | |
| else: | |
| signals.append("Below SMA20 (weakness)") | |
| news_sentiment_str = f"News Sentiment: {news_sentiment.capitalize()} (e.g., {news[0] if news else 'No updates'})" | |
| action_bias = "favorable for buying" if action == "buy" and trend == "upward" else "favorable for selling" if action == "sell" and trend == "downward" else "mixed" | |
| features = [ | |
| f"Trend: {trend} ({change_pct:.1f}% change)", | |
| f"Key Signals: {', '.join(signals)}", | |
| news_sentiment_str, | |
| f"Overall Bias for {action}: {action_bias}" | |
| ] | |
| return "; ".join(features) | |
| def calculate_holding_metrics(hist: pd.DataFrame, holding_period: str, current_price: float) -> Dict[str, Any]: | |
| if not holding_period or hist.empty: | |
| return {} | |
| import re | |
| match = re.search(r'(\d+)\s*(month|week|day)', holding_period.lower()) | |
| if match: | |
| num, unit = int(match.group(1)), match.group(2) | |
| days = num * 30 if unit == 'month' else num * 7 if unit == 'week' else num | |
| purchase_date = datetime.now(pytz.timezone('Asia/Kolkata')) - timedelta(days=days) | |
| hist_before = hist[hist.index < purchase_date] | |
| if not hist_before.empty: | |
| purchase_price = hist_before['Close'].iloc[-1] | |
| else: | |
| # Use oldest price in data if exact date not found | |
| purchase_price = hist['Close'].iloc[0] if not hist.empty else 0 | |
| try: | |
| live_price = yf.Ticker(hist.index.name or 'AAPL').info.get('currentPrice', current_price) | |
| if live_price <= 0: | |
| live_price = current_price | |
| except: | |
| live_price = current_price | |
| if purchase_price > 0: | |
| holding_return = ((live_price - purchase_price) / purchase_price) * 100 | |
| else: | |
| holding_return = 0 | |
| return { | |
| "purchase_price": purchase_price, | |
| "holding_return": holding_return, | |
| "days_held": days, | |
| "pnl": "profit" if holding_return > 0 else "loss" | |
| } | |
| return {} | |
| def predict(horizon: str, indicators: Dict[str, Any], hist: pd.DataFrame, news: list, news_sentiment: str = 'neutral') -> str: | |
| if hist.empty or indicators.get("error"): | |
| return "Unable to predict due to lack of data." | |
| rsi = indicators.get('rsi', 50) | |
| macd = indicators.get('macd', 0) | |
| close = hist['Close'] | |
| if horizon == "intraday": | |
| # minute changes for same-day prediction | |
| recent_change = ((close.iloc[-1] - close.iloc[-10]) / close.iloc[-10]) * 100 if len(close) >= 10 else 0 # Last 10 minutes | |
| prob = 70 if (rsi < 40 and macd > 0) else 50 if rsi > 60 else 30 | |
| direction = "rise" if recent_change > 0 else "fall" | |
| pct = abs(recent_change) * 0.8 | |
| sentiment_boost = 1.15 if news_sentiment == 'positive' else 0.85 if news_sentiment == 'negative' else 1.0 | |
| prob = min(95, int(prob * sentiment_boost)) | |
| return f"{prob}% chance of {pct:.1f}% {direction} in the next 1-2 hours (same-day intraday) based on RSI {rsi:.1f}, MACD {macd:.2f}, recent {recent_change:.1f}% change, and {news_sentiment} news." | |
| elif horizon == "scalping": | |
| # Ultra-short minute prediction | |
| recent_change = ((close.iloc[-1] - close.iloc[-5]) / close.iloc[-5]) * 100 if len(close) >= 5 else 0 # Last 5 minutes | |
| prob = 80 if rsi < 35 else 40 | |
| direction = "quick rise" if rsi < 35 else "quick fall" | |
| pct = 1.5 | |
| return f"{prob}% chance of {pct:.1f}% {direction} in the next 5-10 minutes (ultra-short scalping) based on RSI ({rsi:.1f})." | |
| elif horizon == "swing": | |
| # Formula: Weekly changes for 1-4 week prediction | |
| avg_weekly = close.tail(20).pct_change().mean() * 100 if len(close) >= 20 else 0 # Approx weekly | |
| prob = 65 if avg_weekly > 0 and macd > 0 else 45 if avg_weekly < 0 else 50 | |
| direction = "up" if avg_weekly > 0 else "down" | |
| pct = abs(avg_weekly) * 3 | |
| return f"{prob}% chance of {pct:.1f}% {direction} over the next 1-4 weeks (swing trading)." | |
| elif horizon == "momentum": | |
| # Formula: Momentum over weeks | |
| prob = 70 if macd > 0 and rsi < 60 else 45 | |
| direction = "continued rise" if macd > 0 else "fall" | |
| pct = 4.0 | |
| return f"{prob}% chance of {pct:.1f}% {direction} over the next 2-4 weeks (momentum trading) based on MACD ({macd:.2f})." | |
| else: # long_term | |
| # Formula: Monthly changes for months/years | |
| avg_monthly = close.tail(60).pct_change().mean() * 100 if len(close) >= 60 else 0 | |
| prob = 70 if avg_monthly > 1 and macd > 0 else 40 if avg_monthly < 0 else 50 | |
| direction = "growth" if avg_monthly > 0 else "decline" | |
| pct = abs(avg_monthly) * 2 | |
| sentiment_boost = 1.15 if news_sentiment == 'positive' else 0.85 if news_sentiment == 'negative' else 1.0 | |
| prob = min(95, int(prob * sentiment_boost)) | |
| return f"{prob}% chance of {pct:.1f}% {direction} over months (position trading) based on {avg_monthly:.1f}% avg monthly change and {news_sentiment} news." | |
| def hunter_agent(state: TraderState) -> TraderState: | |
| try: | |
| symbol = state['input_symbol'] | |
| action = state['action'] | |
| horizon = state['horizon'] | |
| if state.get('raw_data') and state['raw_data'].get('hist') is not None: | |
| hist = state['raw_data']['hist'] | |
| else: | |
| hist = fetch_data(symbol, horizon) | |
| if state.get('raw_data') is None: | |
| state['raw_data'] = {} | |
| state['raw_data']['hist'] = hist | |
| indicators = compute_indicators(hist, horizon) | |
| news_data = fetch_news_sentiment(symbol) | |
| headlines = news_data['headlines'] | |
| news_sentiment = news_data['sentiment'] | |
| features_summary = summarize_features(indicators, hist, headlines, action, symbol, news_sentiment) | |
| prediction = predict(horizon, indicators, hist, headlines, news_sentiment) | |
| holding_metrics = calculate_holding_metrics(hist, state.get('holding_period'), indicators.get('current_price', 0)) | |
| state['raw_data']['indicators'] = indicators | |
| state['raw_data']['news'] = headlines | |
| state['raw_data']['features'] = features_summary | |
| state['raw_data']['prediction'] = prediction | |
| state['raw_data']['holding'] = holding_metrics | |
| state['raw_data']['news_sentiment'] = news_sentiment | |
| if holding_metrics: | |
| features_summary += f"; Holding: {holding_metrics['days_held']} days, {holding_metrics['holding_return']:.1f}% {holding_metrics['pnl']}" | |
| rsi = indicators.get('rsi', 50) | |
| macd = indicators.get('macd', 0) | |
| if action == "buy": | |
| if rsi < 30 and macd > 0: | |
| state['claim'] = f"Strong buy signal: Oversold RSI ({rsi:.1f}) with bullish MACD ({macd:.2f}) indicates reversal." | |
| elif rsi < 40 or macd > 5: | |
| state['claim'] = f"Moderate buy signal: RSI at {rsi:.1f}, MACD at {macd:.2f} shows positive momentum." | |
| else: | |
| state['claim'] = f"Weak buy signal: RSI ({rsi:.1f}) and MACD ({macd:.2f}) don't strongly support buying now." | |
| else: | |
| if rsi > 70 or macd < -5: | |
| state['claim'] = f"Strong sell signal: Overbought RSI ({rsi:.1f}) or weak MACD ({macd:.2f}) suggests taking profits." | |
| elif rsi > 60 or macd < 0: | |
| state['claim'] = f"Moderate sell signal: RSI at {rsi:.1f}, MACD at {macd:.2f} shows weakening momentum." | |
| else: | |
| state['claim'] = f"Weak sell signal: RSI ({rsi:.1f}) and MACD ({macd:.2f}) don't strongly support selling now." | |
| return state | |
| except Exception as e: | |
| print(f"Error in hunter_agent: {e}") | |
| return state # Always return state | |
| def skeptic_agent(state: TraderState) -> TraderState: | |
| try: | |
| # Use cached raw_data | |
| raw_data = state.get('raw_data', {}) or {} | |
| features_summary = raw_data.get("features", "No features available.") | |
| action = state['action'] | |
| prompt = f"Counter claim '{state.get('claim', '')}' for {action}: {features_summary}. Provide 1 specific risk." | |
| try: | |
| state['skepticism'] = llm.invoke(prompt).content.strip() | |
| except: | |
| rsi = raw_data.get("indicators", {}).get('rsi', 50) | |
| state['skepticism'] = f"Risk: RSI at {rsi} indicates potential reversal." | |
| return state | |
| except Exception as e: | |
| print(f"Error in skeptic_agent: {e}") | |
| return state # Always return state | |
| def calibrator_agent(state: TraderState) -> TraderState: | |
| try: | |
| raw_data = state.get('raw_data', {}) or {} | |
| indicators = raw_data.get("indicators", {}) | |
| action = state['action'] | |
| news_sentiment = raw_data.get('news_sentiment', 'neutral') | |
| holding = raw_data.get('holding', {}) | |
| state['iterations'] = state.get('iterations', 0) + 1 | |
| rsi = indicators.get('rsi', 50) | |
| macd = indicators.get('macd', 0) | |
| base = 50 | |
| if action == "buy": | |
| if rsi < 30: | |
| base += 30 | |
| elif rsi < 40: | |
| base += 15 | |
| elif rsi > 70: | |
| base -= 25 | |
| elif rsi > 60: | |
| base -= 10 | |
| if macd > 5: | |
| base += 20 | |
| elif macd > 0: | |
| base += 10 | |
| elif macd < -5: | |
| base -= 20 | |
| elif macd < 0: | |
| base -= 10 | |
| if news_sentiment == 'positive': | |
| base += 15 | |
| elif news_sentiment == 'negative': | |
| base -= 15 | |
| else: | |
| if rsi > 70: | |
| base += 30 | |
| elif rsi > 60: | |
| base += 15 | |
| elif rsi < 30: | |
| base -= 25 | |
| elif rsi < 40: | |
| base -= 10 | |
| if macd < -5: | |
| base += 20 | |
| elif macd < 0: | |
| base += 10 | |
| elif macd > 5: | |
| base -= 20 | |
| elif macd > 0: | |
| base -= 10 | |
| if news_sentiment == 'negative': | |
| base += 15 | |
| elif news_sentiment == 'positive': | |
| base -= 15 | |
| if holding: | |
| pnl_pct = holding.get('holding_return', 0) | |
| if action == "sell": | |
| if pnl_pct > 10: | |
| base += 15 | |
| elif pnl_pct > 5: | |
| base += 10 | |
| elif pnl_pct < -10: | |
| base += 20 | |
| elif pnl_pct < -5: | |
| base += 10 | |
| state['confidence'] = min(95, max(5, base)) | |
| state['stop'] = state['iterations'] >= 5 or state['confidence'] >= 85 or state['confidence'] <= 15 | |
| return state | |
| except Exception as e: | |
| print(f"Error in calibrator_agent: {e}") | |
| return state |