from typing import Annotated from datetime import datetime from dateutil.relativedelta import relativedelta import yfinance as yf import pandas as pd import os from .stockstats_utils import StockstatsUtils def get_YFin_data_online( 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"], ): datetime.strptime(start_date, "%Y-%m-%d") datetime.strptime(end_date, "%Y-%m-%d") # Create ticker object ticker = yf.Ticker(symbol.upper()) # Fetch historical data for the specified date range data = ticker.history(start=start_date, end=end_date) # Check if data is empty if data.empty: return ( f"No data found for symbol '{symbol}' between {start_date} and {end_date}" ) # Remove timezone info from index for cleaner output if data.index.tz is not None: data.index = data.index.tz_localize(None) # Round numerical values to 2 decimal places for cleaner display numeric_columns = ["Open", "High", "Low", "Close", "Adj Close"] for col in numeric_columns: if col in data.columns: data[col] = data[col].round(2) # Convert DataFrame to CSV string csv_string = data.to_csv() # Add header information header = f"# Stock data for {symbol.upper()} 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" return header + csv_string def get_stock_stats_indicators_window( symbol: Annotated[str, "ticker symbol of the company"], indicator: Annotated[str, "technical indicator to get the analysis and report of"], curr_date: Annotated[ str, "The current trading date you are trading on, YYYY-mm-dd" ], look_back_days: Annotated[int, "how many days to look back"], ) -> str: best_ind_params = { # Moving Averages "close_50_sma": ( "50 SMA: A medium-term trend indicator. " "Usage: Identify trend direction and serve as dynamic support/resistance. " "Tips: It lags price; combine with faster indicators for timely signals." ), "close_200_sma": ( "200 SMA: A long-term trend benchmark. " "Usage: Confirm overall market trend and identify golden/death cross setups. " "Tips: It reacts slowly; best for strategic trend confirmation rather than frequent trading entries." ), "close_10_ema": ( "10 EMA: A responsive short-term average. " "Usage: Capture quick shifts in momentum and potential entry points. " "Tips: Prone to noise in choppy markets; use alongside longer averages for filtering false signals." ), # MACD Related "macd": ( "MACD: Computes momentum via differences of EMAs. " "Usage: Look for crossovers and divergence as signals of trend changes. " "Tips: Confirm with other indicators in low-volatility or sideways markets." ), "macds": ( "MACD Signal: An EMA smoothing of the MACD line. " "Usage: Use crossovers with the MACD line to trigger trades. " "Tips: Should be part of a broader strategy to avoid false positives." ), "macdh": ( "MACD Histogram: Shows the gap between the MACD line and its signal. " "Usage: Visualize momentum strength and spot divergence early. " "Tips: Can be volatile; complement with additional filters in fast-moving markets." ), # Momentum Indicators "rsi": ( "RSI: Measures momentum to flag overbought/oversold conditions. " "Usage: Apply 70/30 thresholds and watch for divergence to signal reversals. " "Tips: In strong trends, RSI may remain extreme; always cross-check with trend analysis." ), # Volatility Indicators "boll": ( "Bollinger Middle: A 20 SMA serving as the basis for Bollinger Bands. " "Usage: Acts as a dynamic benchmark for price movement. " "Tips: Combine with the upper and lower bands to effectively spot breakouts or reversals." ), "boll_ub": ( "Bollinger Upper Band: Typically 2 standard deviations above the middle line. " "Usage: Signals potential overbought conditions and breakout zones. " "Tips: Confirm signals with other tools; prices may ride the band in strong trends." ), "boll_lb": ( "Bollinger Lower Band: Typically 2 standard deviations below the middle line. " "Usage: Indicates potential oversold conditions. " "Tips: Use additional analysis to avoid false reversal signals." ), "atr": ( "ATR: Averages true range to measure volatility. " "Usage: Set stop-loss levels and adjust position sizes based on current market volatility. " "Tips: It's a reactive measure, so use it as part of a broader risk management strategy." ), # Volume-Based Indicators "vwma": ( "VWMA: A moving average weighted by volume. " "Usage: Confirm trends by integrating price action with volume data. " "Tips: Watch for skewed results from volume spikes; use in combination with other volume analyses." ), "mfi": ( "MFI: The Money Flow Index is a momentum indicator that uses both price and volume to measure buying and selling pressure. " "Usage: Identify overbought (>80) or oversold (<20) conditions and confirm the strength of trends or reversals. " "Tips: Use alongside RSI or MACD to confirm signals; divergence between price and MFI can indicate potential reversals." ), } if indicator not in best_ind_params: raise ValueError( f"Indicator {indicator} is not supported. Please choose from: {list(best_ind_params.keys())}" ) end_date = curr_date curr_date_dt = datetime.strptime(curr_date, "%Y-%m-%d") before = curr_date_dt - relativedelta(days=look_back_days) # Optimized: Get stock data once and calculate indicators for all dates try: indicator_data = _get_stock_stats_bulk(symbol, indicator, curr_date) # Generate the date range we need current_dt = curr_date_dt date_values = [] while current_dt >= before: date_str = current_dt.strftime('%Y-%m-%d') # Look up the indicator value for this date if date_str in indicator_data: indicator_value = indicator_data[date_str] else: indicator_value = "N/A: Not a trading day (weekend or holiday)" date_values.append((date_str, indicator_value)) current_dt = current_dt - relativedelta(days=1) # Build the result string ind_string = "" for date_str, value in date_values: ind_string += f"{date_str}: {value}\n" except Exception as e: print(f"Error getting bulk stockstats data: {e}") # Fallback to original implementation if bulk method fails ind_string = "" curr_date_dt = datetime.strptime(curr_date, "%Y-%m-%d") while curr_date_dt >= before: indicator_value = get_stockstats_indicator( symbol, indicator, curr_date_dt.strftime("%Y-%m-%d") ) ind_string += f"{curr_date_dt.strftime('%Y-%m-%d')}: {indicator_value}\n" curr_date_dt = curr_date_dt - relativedelta(days=1) result_str = ( f"## {indicator} values from {before.strftime('%Y-%m-%d')} to {end_date}:\n\n" + ind_string + "\n\n" + best_ind_params.get(indicator, "No description available.") ) return result_str def _get_stock_stats_bulk( symbol: Annotated[str, "ticker symbol of the company"], indicator: Annotated[str, "technical indicator to calculate"], curr_date: Annotated[str, "current date for reference"] ) -> dict: """ Optimized bulk calculation of stock stats indicators. Fetches data once and calculates indicator for all available dates. Returns dict mapping date strings to indicator values. """ from .config import get_config import pandas as pd from stockstats import wrap import os config = get_config() online = config["data_vendors"]["technical_indicators"] != "local" if not online: # Local data path try: data = pd.read_csv( os.path.join( config.get("data_cache_dir", "data"), f"{symbol}-YFin-data-2015-01-01-2025-03-25.csv", ) ) df = wrap(data) except FileNotFoundError: raise Exception("Stockstats fail: Yahoo Finance data not fetched yet!") else: # Online data fetching with caching today_date = pd.Timestamp.today() curr_date_dt = pd.to_datetime(curr_date) end_date = today_date start_date = today_date - pd.DateOffset(years=2) start_date_str = start_date.strftime("%Y-%m-%d") end_date_str = end_date.strftime("%Y-%m-%d") os.makedirs(config["data_cache_dir"], exist_ok=True) data_file = os.path.join( config["data_cache_dir"], f"{symbol}-YFin-data-{start_date_str}-{end_date_str}.csv", ) if os.path.exists(data_file): data = pd.read_csv(data_file) data["Date"] = pd.to_datetime(data["Date"]) else: data = yf.download( symbol, start=start_date_str, end=end_date_str, multi_level_index=False, progress=False, auto_adjust=True, ) data = data.reset_index() data.to_csv(data_file, index=False) df = wrap(data) df["Date"] = df["Date"].dt.strftime("%Y-%m-%d") # Calculate the indicator for all rows at once df[indicator] # This triggers stockstats to calculate the indicator # Create a dictionary mapping date strings to indicator values result_dict = {} for _, row in df.iterrows(): date_str = row["Date"] indicator_value = row[indicator] # Handle NaN/None values if pd.isna(indicator_value): result_dict[date_str] = "N/A" else: result_dict[date_str] = str(indicator_value) return result_dict def get_stockstats_indicator( symbol: Annotated[str, "ticker symbol of the company"], indicator: Annotated[str, "technical indicator to get the analysis and report of"], curr_date: Annotated[ str, "The current trading date you are trading on, YYYY-mm-dd" ], ) -> str: curr_date_dt = datetime.strptime(curr_date, "%Y-%m-%d") curr_date = curr_date_dt.strftime("%Y-%m-%d") try: indicator_value = StockstatsUtils.get_stock_stats( symbol, indicator, curr_date, ) except Exception as e: print( f"Error getting stockstats indicator data for indicator {indicator} on {curr_date}: {e}" ) return "" return str(indicator_value) def get_technical_analysis( symbol: Annotated[str, "ticker symbol of the company"], curr_date: Annotated[str, "The current trading date, YYYY-mm-dd"], ) -> str: """ Get a concise technical analysis summary with key indicators, signals, and trend interpretation. Returns analysis-ready output instead of verbose day-by-day data. """ from .config import get_config from stockstats import wrap # Default indicators to analyze indicators = ["rsi", "stoch", "macd", "adx", "close_20_ema", "close_50_sma", "close_200_sma", "boll", "atr", "obv", "vwap", "fib"] # Fetch price data (last 60 days for indicator calculation) curr_date_dt = pd.to_datetime(curr_date) start_date = curr_date_dt - pd.DateOffset(days=200) # Need enough history for 200 SMA try: data = yf.download( symbol, start=start_date.strftime("%Y-%m-%d"), end=curr_date_dt.strftime("%Y-%m-%d"), multi_level_index=False, progress=False, auto_adjust=True, ) if data.empty: return f"No data found for {symbol}" data = data.reset_index() df = wrap(data) # Get latest values latest = df.iloc[-1] prev = df.iloc[-2] if len(df) > 1 else latest prev_5 = df.iloc[-5] if len(df) > 5 else latest current_price = float(latest['close']) # Build analysis analysis = [] analysis.append(f"# Technical Analysis for {symbol.upper()}") analysis.append(f"**Date:** {curr_date}") analysis.append(f"**Current Price:** ${current_price:.2f}") analysis.append("") # Price action summary daily_change = ((current_price - float(prev['close'])) / float(prev['close'])) * 100 weekly_change = ((current_price - float(prev_5['close'])) / float(prev_5['close'])) * 100 analysis.append(f"## Price Action") analysis.append(f"- **Daily Change:** {daily_change:+.2f}%") analysis.append(f"- **5-Day Change:** {weekly_change:+.2f}%") analysis.append("") # RSI Analysis if 'rsi' in indicators: try: df['rsi'] # Trigger calculation rsi = float(df.iloc[-1]['rsi']) rsi_prev = float(df.iloc[-5]['rsi']) if len(df) > 5 else rsi if rsi > 70: rsi_signal = "OVERBOUGHT ⚠️" elif rsi < 30: rsi_signal = "OVERSOLD ⚡" elif rsi > 50: rsi_signal = "Bullish" else: rsi_signal = "Bearish" rsi_trend = "↑" if rsi > rsi_prev else "↓" analysis.append(f"## RSI (14)") analysis.append(f"- **Value:** {rsi:.1f} {rsi_trend}") analysis.append(f"- **Signal:** {rsi_signal}") analysis.append("") except Exception as e: pass # MACD Analysis if 'macd' in indicators: try: df['macd'] df['macds'] df['macdh'] macd = float(df.iloc[-1]['macd']) signal = float(df.iloc[-1]['macds']) histogram = float(df.iloc[-1]['macdh']) hist_prev = float(df.iloc[-2]['macdh']) if len(df) > 1 else histogram if macd > signal and histogram > 0: macd_signal = "BULLISH CROSSOVER ⚡" if histogram > hist_prev else "Bullish" elif macd < signal and histogram < 0: macd_signal = "BEARISH CROSSOVER ⚠️" if histogram < hist_prev else "Bearish" else: macd_signal = "Neutral" momentum = "Strengthening ↑" if abs(histogram) > abs(hist_prev) else "Weakening ↓" analysis.append(f"## MACD") analysis.append(f"- **MACD Line:** {macd:.3f}") analysis.append(f"- **Signal Line:** {signal:.3f}") analysis.append(f"- **Histogram:** {histogram:.3f} ({momentum})") analysis.append(f"- **Signal:** {macd_signal}") analysis.append("") except Exception as e: pass # Moving Averages if 'close_50_sma' in indicators or 'close_200_sma' in indicators: try: df['close_50_sma'] df['close_200_sma'] sma_50 = float(df.iloc[-1]['close_50_sma']) sma_200 = float(df.iloc[-1]['close_200_sma']) # Trend determination if current_price > sma_50 > sma_200: trend = "STRONG UPTREND ⚡" elif current_price > sma_50: trend = "Uptrend" elif current_price < sma_50 < sma_200: trend = "STRONG DOWNTREND ⚠️" elif current_price < sma_50: trend = "Downtrend" else: trend = "Sideways" # Golden/Death cross detection sma_50_prev = float(df.iloc[-5]['close_50_sma']) if len(df) > 5 else sma_50 sma_200_prev = float(df.iloc[-5]['close_200_sma']) if len(df) > 5 else sma_200 cross = "" if sma_50 > sma_200 and sma_50_prev < sma_200_prev: cross = " (GOLDEN CROSS ⚡)" elif sma_50 < sma_200 and sma_50_prev > sma_200_prev: cross = " (DEATH CROSS ⚠️)" analysis.append(f"## Moving Averages") analysis.append(f"- **50 SMA:** ${sma_50:.2f} ({'+' if current_price > sma_50 else ''}{((current_price - sma_50) / sma_50 * 100):.1f}% from price)") analysis.append(f"- **200 SMA:** ${sma_200:.2f} ({'+' if current_price > sma_200 else ''}{((current_price - sma_200) / sma_200 * 100):.1f}% from price)") analysis.append(f"- **Trend:** {trend}{cross}") analysis.append("") except Exception as e: pass # Bollinger Bands if 'boll' in indicators: try: df['boll'] df['boll_ub'] df['boll_lb'] middle = float(df.iloc[-1]['boll']) upper = float(df.iloc[-1]['boll_ub']) lower = float(df.iloc[-1]['boll_lb']) # Position within bands (0 = lower, 1 = upper) band_position = (current_price - lower) / (upper - lower) if upper != lower else 0.5 if band_position > 0.95: bb_signal = "AT UPPER BAND - Potential reversal ⚠️" elif band_position < 0.05: bb_signal = "AT LOWER BAND - Potential bounce ⚡" elif band_position > 0.8: bb_signal = "Near upper band" elif band_position < 0.2: bb_signal = "Near lower band" else: bb_signal = "Within bands" bandwidth = ((upper - lower) / middle) * 100 analysis.append(f"## Bollinger Bands (20,2)") analysis.append(f"- **Upper:** ${upper:.2f}") analysis.append(f"- **Middle:** ${middle:.2f}") analysis.append(f"- **Lower:** ${lower:.2f}") analysis.append(f"- **Band Position:** {band_position:.0%}") analysis.append(f"- **Bandwidth:** {bandwidth:.1f}% (volatility indicator)") analysis.append(f"- **Signal:** {bb_signal}") analysis.append("") except Exception as e: pass # ATR (Volatility) if 'atr' in indicators: try: df['atr'] atr = float(df.iloc[-1]['atr']) atr_pct = (atr / current_price) * 100 if atr_pct > 5: vol_level = "HIGH VOLATILITY ⚠️" elif atr_pct > 2: vol_level = "Moderate volatility" else: vol_level = "Low volatility" analysis.append(f"## ATR (Volatility)") analysis.append(f"- **ATR:** ${atr:.2f} ({atr_pct:.1f}% of price)") analysis.append(f"- **Level:** {vol_level}") analysis.append(f"- **Suggested Stop-Loss:** ${current_price - (1.5 * atr):.2f} (1.5x ATR)") analysis.append("") except Exception as e: pass # Stochastic Oscillator if 'stoch' in indicators: try: df['kdjk'] # Stochastic %K df['kdjd'] # Stochastic %D stoch_k = float(df.iloc[-1]['kdjk']) stoch_d = float(df.iloc[-1]['kdjd']) stoch_k_prev = float(df.iloc[-2]['kdjk']) if len(df) > 1 else stoch_k if stoch_k > 80 and stoch_d > 80: stoch_signal = "OVERBOUGHT ⚠️" elif stoch_k < 20 and stoch_d < 20: stoch_signal = "OVERSOLD ⚡" elif stoch_k > stoch_d and stoch_k_prev < stoch_d: stoch_signal = "Bullish crossover ⚡" elif stoch_k < stoch_d and stoch_k_prev > stoch_d: stoch_signal = "Bearish crossover ⚠️" elif stoch_k > 50: stoch_signal = "Bullish" else: stoch_signal = "Bearish" analysis.append(f"## Stochastic (14,3,3)") analysis.append(f"- **%K:** {stoch_k:.1f}") analysis.append(f"- **%D:** {stoch_d:.1f}") analysis.append(f"- **Signal:** {stoch_signal}") analysis.append("") except Exception as e: pass # ADX (Trend Strength) if 'adx' in indicators: try: df['adx'] df['dx'] adx = float(df.iloc[-1]['adx']) adx_prev = float(df.iloc[-5]['adx']) if len(df) > 5 else adx if adx > 50: trend_strength = "VERY STRONG TREND ⚡" elif adx > 25: trend_strength = "Strong trend" elif adx > 20: trend_strength = "Trending" else: trend_strength = "WEAK/NO TREND (range-bound) ⚠️" adx_direction = "Strengthening ↑" if adx > adx_prev else "Weakening ↓" analysis.append(f"## ADX (Trend Strength)") analysis.append(f"- **ADX:** {adx:.1f} ({adx_direction})") analysis.append(f"- **Interpretation:** {trend_strength}") analysis.append("") except Exception as e: pass # 20 EMA (Short-term trend) if 'close_20_ema' in indicators: try: df['close_20_ema'] ema_20 = float(df.iloc[-1]['close_20_ema']) pct_from_ema = ((current_price - ema_20) / ema_20) * 100 if current_price > ema_20: ema_signal = "Price ABOVE 20 EMA (short-term bullish)" else: ema_signal = "Price BELOW 20 EMA (short-term bearish)" analysis.append(f"## 20 EMA") analysis.append(f"- **Value:** ${ema_20:.2f} ({pct_from_ema:+.1f}% from price)") analysis.append(f"- **Signal:** {ema_signal}") analysis.append("") except Exception as e: pass # OBV (On-Balance Volume) if 'obv' in indicators: try: # Calculate OBV manually since stockstats may not have it obv = 0 obv_values = [0] for i in range(1, len(df)): if float(df.iloc[i]['close']) > float(df.iloc[i-1]['close']): obv += float(df.iloc[i]['volume']) elif float(df.iloc[i]['close']) < float(df.iloc[i-1]['close']): obv -= float(df.iloc[i]['volume']) obv_values.append(obv) current_obv = obv_values[-1] obv_5_ago = obv_values[-5] if len(obv_values) > 5 else obv_values[0] if current_obv > obv_5_ago and current_price > float(df.iloc[-5]['close']): obv_signal = "Confirmed uptrend (price & volume rising)" elif current_obv < obv_5_ago and current_price < float(df.iloc[-5]['close']): obv_signal = "Confirmed downtrend (price & volume falling)" elif current_obv > obv_5_ago and current_price < float(df.iloc[-5]['close']): obv_signal = "BULLISH DIVERGENCE ⚡ (accumulation)" elif current_obv < obv_5_ago and current_price > float(df.iloc[-5]['close']): obv_signal = "BEARISH DIVERGENCE ⚠️ (distribution)" else: obv_signal = "Neutral" obv_formatted = f"{current_obv/1e6:.1f}M" if abs(current_obv) > 1e6 else f"{current_obv/1e3:.1f}K" analysis.append(f"## OBV (On-Balance Volume)") analysis.append(f"- **Value:** {obv_formatted}") analysis.append(f"- **5-Day Trend:** {'Rising ↑' if current_obv > obv_5_ago else 'Falling ↓'}") analysis.append(f"- **Signal:** {obv_signal}") analysis.append("") except Exception as e: pass # VWAP (Volume Weighted Average Price) if 'vwap' in indicators: try: # Calculate VWAP for today (simplified - using recent data) typical_price = (float(df.iloc[-1]['high']) + float(df.iloc[-1]['low']) + float(df.iloc[-1]['close'])) / 3 # Calculate cumulative VWAP (last 20 periods approximation) recent_df = df.tail(20) tp_vol = ((recent_df['high'] + recent_df['low'] + recent_df['close']) / 3) * recent_df['volume'] vwap = float(tp_vol.sum() / recent_df['volume'].sum()) pct_from_vwap = ((current_price - vwap) / vwap) * 100 if current_price > vwap: vwap_signal = "Price ABOVE VWAP (institutional buying)" else: vwap_signal = "Price BELOW VWAP (institutional selling)" analysis.append(f"## VWAP (20-period)") analysis.append(f"- **VWAP:** ${vwap:.2f}") analysis.append(f"- **Current vs VWAP:** {pct_from_vwap:+.1f}%") analysis.append(f"- **Signal:** {vwap_signal}") analysis.append("") except Exception as e: pass # Fibonacci Retracement Levels if 'fib' in indicators: try: # Get high and low from last 50 periods recent_high = float(df.tail(50)['high'].max()) recent_low = float(df.tail(50)['low'].min()) diff = recent_high - recent_low fib_levels = { "0.0% (High)": recent_high, "23.6%": recent_high - (diff * 0.236), "38.2%": recent_high - (diff * 0.382), "50.0%": recent_high - (diff * 0.5), "61.8%": recent_high - (diff * 0.618), "78.6%": recent_high - (diff * 0.786), "100% (Low)": recent_low, } # Find nearest support and resistance support = None resistance = None for level_name, level_price in fib_levels.items(): if level_price < current_price and (support is None or level_price > support[1]): support = (level_name, level_price) if level_price > current_price and (resistance is None or level_price < resistance[1]): resistance = (level_name, level_price) analysis.append(f"## Fibonacci Levels (50-period)") analysis.append(f"- **Recent High:** ${recent_high:.2f}") analysis.append(f"- **Recent Low:** ${recent_low:.2f}") if resistance: analysis.append(f"- **Next Resistance:** ${resistance[1]:.2f} ({resistance[0]})") if support: analysis.append(f"- **Next Support:** ${support[1]:.2f} ({support[0]})") analysis.append("") except Exception as e: pass # Overall Summary analysis.append("## Summary") signals = [] # Collect all signals for summary try: rsi = float(df.iloc[-1]['rsi']) if rsi > 70: signals.append("RSI overbought") elif rsi < 30: signals.append("RSI oversold") except: pass try: if current_price > float(df.iloc[-1]['close_50_sma']): signals.append("Above 50 SMA") else: signals.append("Below 50 SMA") except: pass if signals: analysis.append(f"- **Key Signals:** {', '.join(signals)}") return "\n".join(analysis) except Exception as e: return f"Error analyzing {symbol}: {str(e)}" def get_balance_sheet( ticker: Annotated[str, "ticker symbol of the company"], freq: Annotated[str, "frequency of data: 'annual' or 'quarterly'"] = "quarterly", curr_date: Annotated[str, "current date (not used for yfinance)"] = None ): """Get balance sheet data from yfinance.""" try: ticker_obj = yf.Ticker(ticker.upper()) if freq.lower() == "quarterly": data = ticker_obj.quarterly_balance_sheet else: data = ticker_obj.balance_sheet if data.empty: return f"No balance sheet data found for symbol '{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"# 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)}" def get_cashflow( ticker: Annotated[str, "ticker symbol of the company"], freq: Annotated[str, "frequency of data: 'annual' or 'quarterly'"] = "quarterly", curr_date: Annotated[str, "current date (not used for yfinance)"] = None ): """Get cash flow data from yfinance.""" try: ticker_obj = yf.Ticker(ticker.upper()) if freq.lower() == "quarterly": data = ticker_obj.quarterly_cashflow else: data = ticker_obj.cashflow if data.empty: return f"No cash flow data found for symbol '{ticker}'" # 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"# 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)}" def get_income_statement( ticker: Annotated[str, "ticker symbol of the company"], freq: Annotated[str, "frequency of data: 'annual' or 'quarterly'"] = "quarterly", curr_date: Annotated[str, "current date (not used for yfinance)"] = None ): """Get income statement data from yfinance.""" try: ticker_obj = yf.Ticker(ticker.upper()) if freq.lower() == "quarterly": data = ticker_obj.quarterly_income_stmt else: data = ticker_obj.income_stmt if data.empty: return f"No income statement data found for symbol '{ticker}'" # 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"# 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)}" def get_insider_transactions( ticker: Annotated[str, "ticker symbol of the company"], curr_date: Annotated[str, "current date (not used for yfinance)"] = None ): """Get insider transactions data from yfinance with parsed transaction types.""" try: ticker_obj = yf.Ticker(ticker.upper()) data = ticker_obj.insider_transactions if data is None or data.empty: return f"No insider transactions data found for symbol '{ticker}'" # Parse the Text column to populate Transaction type def classify_transaction(text): if pd.isna(text) or text == '': return 'Unknown' text_lower = str(text).lower() if 'sale' in text_lower: return 'Sale' elif 'purchase' in text_lower or 'buy' in text_lower: return 'Purchase' elif 'gift' in text_lower: return 'Gift' elif 'exercise' in text_lower or 'option' in text_lower: return 'Option Exercise' elif 'award' in text_lower or 'grant' in text_lower: return 'Award/Grant' elif 'conversion' in text_lower: return 'Conversion' else: return 'Other' # Apply classification data['Transaction'] = data['Text'].apply(classify_transaction) # Calculate summary statistics transaction_counts = data['Transaction'].value_counts().to_dict() total_sales_value = data[data['Transaction'] == 'Sale']['Value'].sum() total_purchases_value = data[data['Transaction'] == 'Purchase']['Value'].sum() # Determine insider sentiment sales_count = transaction_counts.get('Sale', 0) purchases_count = transaction_counts.get('Purchase', 0) if purchases_count > sales_count: sentiment = "BULLISH ⚡ (more buying than selling)" elif sales_count > purchases_count * 2: sentiment = "BEARISH ⚠️ (significant insider selling)" elif sales_count > purchases_count: sentiment = "Slightly bearish (more selling than buying)" else: sentiment = "Neutral" # Build summary header header = f"# Insider Transactions for {ticker.upper()}\n" header += f"# Data retrieved on: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}\n\n" header += "## Summary\n" header += f"- **Insider Sentiment:** {sentiment}\n" for tx_type, count in sorted(transaction_counts.items(), key=lambda x: -x[1]): header += f"- **{tx_type}:** {count} transactions\n" if total_sales_value > 0: header += f"- **Total Sales Value:** ${total_sales_value:,.0f}\n" if total_purchases_value > 0: header += f"- **Total Purchases Value:** ${total_purchases_value:,.0f}\n" header += "\n## Transaction Details\n\n" # Select key columns for output output_cols = ['Start Date', 'Insider', 'Position', 'Transaction', 'Shares', 'Value', 'Ownership'] available_cols = [c for c in output_cols if c in data.columns] csv_string = data[available_cols].to_csv(index=False) return header + csv_string except Exception as e: return f"Error retrieving insider transactions for {ticker}: {str(e)}" def validate_ticker(symbol: str) -> bool: """ Validate if a ticker symbol exists and has trading data. """ try: ticker = yf.Ticker(symbol.upper()) # Use fast_info for lighter validation (no historical download needed) # fast_info attributes are lazy-loaded _ = ticker.fast_info.get("lastPrice") return True except Exception: # Fallback to older method if fast_info fails or is missing try: return not ticker.history(period="1d", progress=False).empty except: return False def get_fundamentals( ticker: Annotated[str, "ticker symbol of the company"], curr_date: Annotated[str, "current date (for reference)"] = None ) -> str: """ Get comprehensive fundamental data for a ticker using yfinance. Returns data in a format similar to Alpha Vantage's OVERVIEW endpoint. This is a FREE alternative to Alpha Vantage with no rate limits. """ import json try: ticker_obj = yf.Ticker(ticker.upper()) info = ticker_obj.info if not info or info.get('regularMarketPrice') is None: return f"No fundamental data found for symbol '{ticker}'" # Build a structured response similar to Alpha Vantage fundamentals = { # Company Info "Symbol": ticker.upper(), "AssetType": info.get("quoteType", "N/A"), "Name": info.get("longName", info.get("shortName", "N/A")), "Description": info.get("longBusinessSummary", "N/A"), "Exchange": info.get("exchange", "N/A"), "Currency": info.get("currency", "USD"), "Country": info.get("country", "N/A"), "Sector": info.get("sector", "N/A"), "Industry": info.get("industry", "N/A"), "Address": f"{info.get('address1', '')} {info.get('city', '')}, {info.get('state', '')} {info.get('zip', '')}".strip(), "OfficialSite": info.get("website", "N/A"), "FiscalYearEnd": info.get("fiscalYearEnd", "N/A"), # Valuation "MarketCapitalization": str(info.get("marketCap", "N/A")), "EBITDA": str(info.get("ebitda", "N/A")), "PERatio": str(info.get("trailingPE", "N/A")), "ForwardPE": str(info.get("forwardPE", "N/A")), "PEGRatio": str(info.get("pegRatio", "N/A")), "BookValue": str(info.get("bookValue", "N/A")), "PriceToBookRatio": str(info.get("priceToBook", "N/A")), "PriceToSalesRatioTTM": str(info.get("priceToSalesTrailing12Months", "N/A")), "EVToRevenue": str(info.get("enterpriseToRevenue", "N/A")), "EVToEBITDA": str(info.get("enterpriseToEbitda", "N/A")), # Earnings & Revenue "EPS": str(info.get("trailingEps", "N/A")), "ForwardEPS": str(info.get("forwardEps", "N/A")), "RevenueTTM": str(info.get("totalRevenue", "N/A")), "RevenuePerShareTTM": str(info.get("revenuePerShare", "N/A")), "GrossProfitTTM": str(info.get("grossProfits", "N/A")), "QuarterlyRevenueGrowthYOY": str(info.get("revenueGrowth", "N/A")), "QuarterlyEarningsGrowthYOY": str(info.get("earningsGrowth", "N/A")), # Margins & Returns "ProfitMargin": str(info.get("profitMargins", "N/A")), "OperatingMarginTTM": str(info.get("operatingMargins", "N/A")), "GrossMargins": str(info.get("grossMargins", "N/A")), "ReturnOnAssetsTTM": str(info.get("returnOnAssets", "N/A")), "ReturnOnEquityTTM": str(info.get("returnOnEquity", "N/A")), # Dividend "DividendPerShare": str(info.get("dividendRate", "N/A")), "DividendYield": str(info.get("dividendYield", "N/A")), "ExDividendDate": str(info.get("exDividendDate", "N/A")), "PayoutRatio": str(info.get("payoutRatio", "N/A")), # Balance Sheet "TotalCash": str(info.get("totalCash", "N/A")), "TotalDebt": str(info.get("totalDebt", "N/A")), "CurrentRatio": str(info.get("currentRatio", "N/A")), "QuickRatio": str(info.get("quickRatio", "N/A")), "DebtToEquity": str(info.get("debtToEquity", "N/A")), "FreeCashFlow": str(info.get("freeCashflow", "N/A")), "OperatingCashFlow": str(info.get("operatingCashflow", "N/A")), # Trading Info "Beta": str(info.get("beta", "N/A")), "52WeekHigh": str(info.get("fiftyTwoWeekHigh", "N/A")), "52WeekLow": str(info.get("fiftyTwoWeekLow", "N/A")), "50DayMovingAverage": str(info.get("fiftyDayAverage", "N/A")), "200DayMovingAverage": str(info.get("twoHundredDayAverage", "N/A")), "SharesOutstanding": str(info.get("sharesOutstanding", "N/A")), "SharesFloat": str(info.get("floatShares", "N/A")), "SharesShort": str(info.get("sharesShort", "N/A")), "ShortRatio": str(info.get("shortRatio", "N/A")), "ShortPercentOfFloat": str(info.get("shortPercentOfFloat", "N/A")), # Ownership "PercentInsiders": str(info.get("heldPercentInsiders", "N/A")), "PercentInstitutions": str(info.get("heldPercentInstitutions", "N/A")), # Analyst "AnalystTargetPrice": str(info.get("targetMeanPrice", "N/A")), "AnalystTargetHigh": str(info.get("targetHighPrice", "N/A")), "AnalystTargetLow": str(info.get("targetLowPrice", "N/A")), "NumberOfAnalysts": str(info.get("numberOfAnalystOpinions", "N/A")), "RecommendationKey": info.get("recommendationKey", "N/A"), "RecommendationMean": str(info.get("recommendationMean", "N/A")), } # Return as formatted JSON string return json.dumps(fundamentals, indent=4) except Exception as e: return f"Error retrieving fundamentals for {ticker}: {str(e)}" def get_options_activity( ticker: Annotated[str, "ticker symbol of the company"], num_expirations: Annotated[int, "number of nearest expiration dates to analyze"] = 3, curr_date: Annotated[str, "current date (for reference)"] = None ) -> str: """ Get options activity for a specific ticker using yfinance. Analyzes volume, open interest, and put/call ratios. This is a FREE alternative to Tradier with no API key required. """ try: ticker_obj = yf.Ticker(ticker.upper()) # Get available expiration dates expirations = ticker_obj.options if not expirations: return f"No options data available for {ticker}" # Analyze the nearest N expiration dates expirations_to_analyze = expirations[:min(num_expirations, len(expirations))] report = f"## Options Activity for {ticker.upper()}\n\n" report += f"**Available Expirations:** {len(expirations)} dates\n" report += f"**Analyzing:** {', '.join(expirations_to_analyze)}\n\n" total_call_volume = 0 total_put_volume = 0 total_call_oi = 0 total_put_oi = 0 unusual_activity = [] for exp_date in expirations_to_analyze: try: opt = ticker_obj.option_chain(exp_date) calls = opt.calls puts = opt.puts if calls.empty and puts.empty: continue # Calculate totals for this expiration call_vol = calls['volume'].sum() if 'volume' in calls.columns else 0 put_vol = puts['volume'].sum() if 'volume' in puts.columns else 0 call_oi = calls['openInterest'].sum() if 'openInterest' in calls.columns else 0 put_oi = puts['openInterest'].sum() if 'openInterest' in puts.columns else 0 # Handle NaN values call_vol = 0 if pd.isna(call_vol) else int(call_vol) put_vol = 0 if pd.isna(put_vol) else int(put_vol) call_oi = 0 if pd.isna(call_oi) else int(call_oi) put_oi = 0 if pd.isna(put_oi) else int(put_oi) total_call_volume += call_vol total_put_volume += put_vol total_call_oi += call_oi total_put_oi += put_oi # Find unusual activity (high volume relative to OI) for _, row in calls.iterrows(): vol = row.get('volume', 0) oi = row.get('openInterest', 0) if pd.notna(vol) and pd.notna(oi) and oi > 0 and vol > oi * 0.5 and vol > 100: unusual_activity.append({ 'type': 'CALL', 'expiration': exp_date, 'strike': row['strike'], 'volume': int(vol), 'openInterest': int(oi), 'vol_oi_ratio': round(vol / oi, 2) if oi > 0 else 0, 'impliedVolatility': round(row.get('impliedVolatility', 0) * 100, 1) }) for _, row in puts.iterrows(): vol = row.get('volume', 0) oi = row.get('openInterest', 0) if pd.notna(vol) and pd.notna(oi) and oi > 0 and vol > oi * 0.5 and vol > 100: unusual_activity.append({ 'type': 'PUT', 'expiration': exp_date, 'strike': row['strike'], 'volume': int(vol), 'openInterest': int(oi), 'vol_oi_ratio': round(vol / oi, 2) if oi > 0 else 0, 'impliedVolatility': round(row.get('impliedVolatility', 0) * 100, 1) }) except Exception as e: report += f"*Error fetching {exp_date}: {str(e)}*\n" continue # Calculate put/call ratios pc_volume_ratio = round(total_put_volume / total_call_volume, 3) if total_call_volume > 0 else 0 pc_oi_ratio = round(total_put_oi / total_call_oi, 3) if total_call_oi > 0 else 0 # Summary report += "### Summary\n" report += "| Metric | Calls | Puts | Put/Call Ratio |\n" report += "|--------|-------|------|----------------|\n" report += f"| Volume | {total_call_volume:,} | {total_put_volume:,} | {pc_volume_ratio} |\n" report += f"| Open Interest | {total_call_oi:,} | {total_put_oi:,} | {pc_oi_ratio} |\n\n" # Sentiment interpretation report += "### Sentiment Analysis\n" if pc_volume_ratio < 0.7: report += "- **Volume P/C Ratio:** Bullish (more call volume)\n" elif pc_volume_ratio > 1.3: report += "- **Volume P/C Ratio:** Bearish (more put volume)\n" else: report += "- **Volume P/C Ratio:** Neutral\n" if pc_oi_ratio < 0.7: report += "- **OI P/C Ratio:** Bullish positioning\n" elif pc_oi_ratio > 1.3: report += "- **OI P/C Ratio:** Bearish positioning\n" else: report += "- **OI P/C Ratio:** Neutral positioning\n" # Unusual activity if unusual_activity: # Sort by volume/OI ratio unusual_activity.sort(key=lambda x: x['vol_oi_ratio'], reverse=True) top_unusual = unusual_activity[:10] report += "\n### Unusual Activity (High Volume vs Open Interest)\n" report += "| Type | Expiry | Strike | Volume | OI | Vol/OI | IV |\n" report += "|------|--------|--------|--------|----|---------|----|---|\n" for item in top_unusual: report += f"| {item['type']} | {item['expiration']} | ${item['strike']} | {item['volume']:,} | {item['openInterest']:,} | {item['vol_oi_ratio']}x | {item['impliedVolatility']}% |\n" else: report += "\n*No unusual options activity detected.*\n" return report except Exception as e: return f"Error retrieving options activity for {ticker}: {str(e)}"