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